# Builin
import os
import sys
import subprocess
import logging.handlers
import json
import socket
if sys.version_info[0] == 2:
from SocketServer import ThreadingMixIn
from SimpleXMLRPCServer import SimpleXMLRPCServer
else:
from socketserver import ThreadingMixIn
from xmlrpc.server import SimpleXMLRPCServer
# Internal
import nxt
from nxt.stage import GraphError
from nxt.remote import get_server_info_filepath
import nxt.remote.contexts
from nxt import nxt_path, nxt_io, nxt_log
server = None
logger = logging.getLogger('nxt')
[docs]class NxtServerException(GraphError):
pass
[docs]class NxtServer(ThreadingMixIn, SimpleXMLRPCServer):
def __init__(self, address, allow_none=True, log_requests=False):
"""Simple Threaded XMLRPCServer.
:param address: tuple of (HOST, PORT)
:param allow_none: If True None type objects are marshaled.
:param log_requests: If True connection requests are logged.
"""
SimpleXMLRPCServer.__init__(self, addr=address, allow_none=allow_none,
logRequests=log_requests)
[docs]class ServerFunctions(object):
def __init__(self, log_filepath):
self.cache_file = None
self.log_filepath = log_filepath
[docs] def get_log_location(self):
"""Simple method for getting the filepath of the file the server is
logging to. May be an empty string if the server isn't logging to a
custom filepath.
:return: str
"""
return self.log_filepath or ''
[docs] @staticmethod
def is_alive():
"""Simple method for clients to use to check if the server is still
alive.
:return: True
"""
logger.info("rpc server is alive and ready")
return True
[docs] @staticmethod
def exec_in_headless(filepath, start_node, cache_path,
parameters, context_name):
"""Executed the given graph (filepath) with the given start_node in the
dcc exe (as a sub-process). The temp path (if provided) must be a
location that the server can read/write. If no temp path is given one
will be generated. The file at the temp path is used to store the
cache data to be returned to the caller. Only the file path is
returned to the caller, not the actual data.
:param filepath: Path to nxt save file
:param start_node: start node path
:param cache_path: Path to store output cache data (if none is given
one will be generated)
:param parameters: Optional parameters dict
:param context_name: name of context, defaults to python
:return: filepath to temp file
"""
# Fixme: Contexts must be accessed like this to avoid importing them
# again and thus emptying the list of user contexts.
context = nxt.remote.contexts.find_context_by_name(context_name)
if not context:
known = nxt.remote.contexts.iter_context_names()
logger.debug('Known contexts: \n{}'.format('\n'.join(known)))
raise NameError('Unknown context "{}"'.format(context_name))
context_exe = context.exe
if not context or not context_exe:
raise TypeError('Unable to find context exe for: '
'{}'.format(context))
context_graph = context.graph
context_graph = nxt_path.full_file_expand(context_graph)
if not context_graph:
raise TypeError('No launch script found for context: '
'{}'.format(context))
# Setup cache file if none provided
if not cache_path:
cache_path = nxt_io.generate_temp_file()
cache_path = cache_path.replace(os.sep, '/')
# Setup parameters file if parameters provided
if parameters:
parameters_file = nxt_io.generate_temp_file()
parameters_file = parameters_file.replace(os.sep, '/')
with open(parameters_file, 'w+') as fp:
json.dump(parameters, fp, separators=(',', ': '))
else:
parameters_file = ''
context_graph = context_graph.replace(os.sep, '/')
logger.info('Starting \n'
'Context: {} \n'
'Interpreter: {} \n'
'Context Graph: {}\n'.format(context, context_exe,
context_graph))
logger.info('Cache location: {}\n'.format(cache_path))
# Format the cli call
safe_graph_path = nxt_path.full_file_expand(filepath)
# open context with graph and parameters
os.environ[nxt_log.VERBOSE_ENV_VAR] = 'socket'
cli_args = ['exec', context_graph, '-p',
'/.graph_file', safe_graph_path,
'/.cache_file', cache_path]
if parameters_file:
cli_args += ['/.parameters_file', parameters_file]
script = os.path.join(os.path.dirname(__file__), '..', 'cli.py')
script = os.path.abspath(script)
script = script.replace(os.sep, '/')
if context.args:
extra_args = []
if context.args:
extra_args = list(context.args)
args = [context_exe] + extra_args + [script, '--'] + cli_args
else:
args = [context_exe, script] + cli_args
if start_node:
args += ['/enter/call_graph._start', start_node]
# HACK solution only until refined context system rolls out to relate
# format strings to context names to include space for cli args.
if 'UE4Editor' in context_exe:
args = context_exe.format(cli_path=script,
cli_args=' '.join(cli_args))
logger.debug('call: {}'.format(args))
# TODO: Find a clean way to raise exceptions from the subprocess.
try:
subprocess.call(args)
except subprocess.CalledProcessError:
raise RuntimeError('Remote context graph "{}" failed! See log...'
''.format(safe_graph_path))
return cache_path
[docs] def kill(self):
"""Shuts down the running rpc server and attemps to remove the server
log file if there is one.
:return: None
"""
global server
if server:
logger.warning('Shutting down rpc server!')
server.shutdown()
if self.log_filepath:
try:
logger.debug('Removing server log file: '
'{}'.format(self.log_filepath))
os.remove(self.log_filepath)
except:
logger.warning('Failed to remove server log file, '
'if it was an autogenerated file nxt_log '
'will remove it when determined it is '
'stale.')
[docs]def secure_address():
server_info_filepath = get_server_info_filepath()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
_, port = s.getsockname()
with open(server_info_filepath, 'w+') as fp:
fp.write('localhost' + '\n')
fp.write(str(port))
return 'localhost', port
[docs]def run_server(address=None, log_requests=False):
nxt_root = os.path.abspath(os.path.join(os.path.dirname(__file__),
'../..')).replace(os.sep, '/')
os.chdir(nxt_root)
global server
if address is None:
address = secure_address()
server = NxtServer(address, log_requests=log_requests)
server.register_instance(ServerFunctions(log_filepath=None))
server.allow_reuse_address = True
logger.info('Threaded nxt rpc server started!')
logger.debug('Listening on: {}'.format(address))
server.serve_forever()
if __name__ == '__main__':
logger.info('Available contexts: '
'{}'.format(list(nxt.remote.contexts.iter_context_names())))
# TODO: Fix logging so non-hosting apps can get the stdout and logs
# logger.debug('Logging to: {}'.format(log_file))
logger.info('Starting up server...')
run_server()
# logger.info('Logging rpc stdout to: {}'.format(log_file))