Source code for nxt.remote.server

# 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))