class Proxy(object): """Class for handling the client side of calls to functionality made available on a server. Parameters ---------- module : str, optional The module from which the requested functionality should be loaded on the server side. python : {'python', 'pythonw'}, optional The type of Python interpreter that should be used to launch the server. url : str, optional The URL from where the requested functionality should be served. Defaults is ``'http://127.0.0.1'``. port : int, optional The port through which the host at the provided URL should be accessed. Default is ``1753``. Notes ----- When a :class:`Proxy` class is instantiated, it tries to establish a connection with the server at the specified address. The default address is ``http://127.0.0.1:1753``. If no server is available at the provided address that support the ``ping_server`` "protokol", it will try to start a new server. If this is unsuccessfull, a ``ThreadedServerError`` will be raised. If a server is available at the specified address, the :class:`Proxy` object will send JSON encoded requests in the following format: * ``'module'``: The fully qualified path of the module from which the functionality should be loaded. * ``'function'``: The name of the function that should be run by the server. * ``'args'`` : A list of positional arguments that should be passed to the remote function. * ``'kwargs'`` : A dict of named arguments that should be passed to the remote function. The modules available to the server for loading functionality depend on the environment in which the subprocess is launched originally started the server. This means that the client side code should be run from the environent that should be available on the server side. Since the module is only used to qualify the requested functionality whenever that functionality is called, the module can be changed at any time to request calls to functionality from different modules. Examples -------- >>> from compas.remote import Proxy >>> p = Proxy() >>> p.ping_server() 1 """ def __init__(self, module=None, python=None, url='http://127.0.0.1', port=1753, service='default'): self._process = None self._service = service self._python = compas._os.select_python( python) # change to pythonw == True self._env = compas._os.prepare_environment() self._url = url self._port = port self.module = module self.function = None self.profile = None self.start_server() @property def address(self): """The address (including the port) of the server host.""" return "{}:{}".format(self._url, self._port) def ping_server(self, attempts=10): """Ping the server to check if it is available at the provided address. Parameters ---------- attempst : int, optional The number of attemps before the function should give up. Default is ``10``. Returns ------- int ``1`` if the server is available. ``0`` otherwise. """ while attempts: try: result = self.send_request('ping') except Exception as e: if compas.IPY: compas_rhino.wait() result = 0 time.sleep(0.1) attempts -= 1 print(" {} attempts left.".format(attempts)) else: break return result def start_server(self): """Start the remote server. Returns ------- ServerProxy Instance of the proxy, if the connection was successful. Raises ------ ThreadedServerError If the server providing the requested service cannot be reached. """ if self.ping_server(): print("Server running at {}...".format(self.address)) return if compas.IPY: self._process = Process() for name in self._env: if self._process.StartInfo.EnvironmentVariables.ContainsKey( name): self._process.StartInfo.EnvironmentVariables[ name] = self._env[name] else: self._process.StartInfo.EnvironmentVariables.Add( name, self._env[name]) self._process.StartInfo.UseShellExecute = False self._process.StartInfo.RedirectStandardOutput = True self._process.StartInfo.RedirectStandardError = True self._process.StartInfo.FileName = self._python self._process.StartInfo.Arguments = '-m {0} {1}'.format( 'compas.remote.services.{}'.format(self._service), str(self._port)) self._process.Start() else: args = [ self._python, '-m', 'compas.remote.services.{}'.format(self._service) ] self._process = Popen(args, stdout=PIPE, stderr=STDOUT, env=self._env) if not self.ping_server(): raise ThreadedServerError("Server unavailable at {}...".format( self.address)) print("Started {} service at {}...".format(self._service, self.address)) def send_request(self, function, module=None, args=None, kwargs=None): """Send a request to the server. Parameters ---------- function : str The name of the function that should be run on the server. module : str, optional The fully qualified name of the module from where the function should be loaded. args : list, optional Positional function arguments. kwargs : dict, optional Named function arguments. Returns ------- object The data returned by the server function. Raises ------ ThreadedServerError If the execution of the function on the server throws an error. Notes ----- The response of the server is a dict with the following items: * ``'error'``: Stores a traceback of any exception thrown during execution of the requested function on the server. * ``'data'``: Stores data returned by the server function. * ``'profile'``: Stores a profile of the function execution. """ idata = { 'function': function, 'module': module, 'args': args, 'kwargs': kwargs } ibody = json.dumps(idata, cls=DataEncoder).encode('utf-8') request = Request(self.address, ibody) request.add_header('Content-Type', 'application/json; charset=utf-8') request.add_header('Content-Length', len(ibody)) response = urlopen(request) obody = response.read().decode('utf-8') odata = json.loads(obody, cls=DataDecoder) self.profile = odata['profile'] if odata['error']: raise ThreadedServerError(odata['error']) return odata['data'] def __getattr__(self, function): self.function = function return self._function_proxy def _function_proxy(self, *args, **kwargs): """""" return self.send_request(self.function, module=self.module, args=args, kwargs=kwargs) def stop_server(self): """""" self.send_request('stop_server') self.terminate_process() def terminate_process(self): """Attempts to terminate the python process hosting the http server. The process reference might not be present, e.g. in the case of reusing an existing connection. In that case, this is a no-op. """ # find the process listening to port 1753? # kill that process instead? if not self._process: return try: self._process.terminate() except: pass try: self._process.kill() except: pass
class Proxy(object): """Create a proxy object as intermediary between client code and remote functionality. This class is a context manager, so when used in a ``with`` statement, it ensures the remote proxy server is stopped and disposed correctly. However, if the proxy server is left open, it can be re-used for a follow-up connection, saving start up time. Parameters ---------- package : string, optional The base package for the requested functionality. Default is `None`, in which case a full path to function calls should be provided. python : string, optional The python executable that should be used to execute the code. Default is ``'pythonw'``. url : string, optional The server address. Default is ``'http://127.0.0.1'``. port : int, optional The port number on the remote server. Default is ``1753``. Notes ----- If the server is your *localhost*, which will often be the case, it is better to specify the address explicitly (``'http://127.0.0.1'``) because resolving *localhost* takes a surprisingly significant amount of time. The service will make the correct (version of the requested) functionality available even if that functionality is part of a virtual environment. This is because it will use the specific python interpreter for which the functionality is installed to start the server. If possible, the proxy will try to reconnect to an already existing service Examples -------- Minimal example showing connection to the proxy server, and ensuring the server is disposed after using it: .. code-block:: python from compas.rpc import Proxy with Proxy('compas.numerical') as numerical: pass Complete example demonstrating use of the force density method in the numerical package to compute equilibrium of axial force networks. .. code-block:: python import compas import time from compas.datastructures import Mesh from compas.rpc import Proxy numerical = Proxy('compas.numerical') mesh = Mesh.from_obj(compas.get('faces.obj')) mesh.update_default_vertex_attributes({'px': 0.0, 'py': 0.0, 'pz': 0.0}) mesh.update_default_edge_attributes({'q': 1.0}) key_index = mesh.key_index() xyz = mesh.get_vertices_attributes('xyz') edges = [(key_index[u], key_index[v]) for u, v in mesh.edges()] fixed = [key_index[key] for key in mesh.vertices_where({'vertex_degree': 2})] q = mesh.get_edges_attribute('q', 1.0) loads = mesh.get_vertices_attributes(('px', 'py', 'pz'), (0.0, 0.0, 0.0)) xyz, q, f, l, r = numerical.fd_numpy(xyz, edges, fixed, q, loads) for key, attr in mesh.vertices(True): index = key attr['x'] = xyz[index][0] attr['y'] = xyz[index][1] attr['z'] = xyz[index][2] attr['rx'] = r[index][0] attr['ry'] = r[index][1] attr['rz'] = r[index][2] for index, (u, v, attr) in enumerate(mesh.edges(True)): attr['f'] = f[index][0] attr['l'] = l[index][0] """ def __init__(self, package=None, python=None, url='http://127.0.0.1', port=1753, service=None): self._package = None self._python = compas._os.select_python(python) self._url = url self._port = port self._service = None self._process = None self._function = None self._profile = None self.service = service self.package = package self._server = self.try_reconnect() if self._server is None: self._server = self.start_server() def __enter__(self): return self def __exit__(self, *args): self.stop_server() @property def address(self): return "{}:{}".format(self._url, self._port) @property def profile(self): """A profile of the executed code.""" return self._profile @profile.setter def profile(self, profile): self._profile = profile @property def package(self): """The base package from which functionality will be called.""" return self._package @package.setter def package(self, package): self._package = package @property def service(self): return self._service @service.setter def service(self, service): if not service: self._service = 'compas.rpc.services.default' else: self._service = service @property def python(self): return self._python @python.setter def python(self, python): self._python = python def try_reconnect(self): """Try and reconnect to an existing proxy server. Returns ------- ServerProxy Instance of the proxy if reconnection succeeded, otherwise ``None``. """ server = ServerProxy(self.address) try: server.ping() except: return None else: print("Reconnecting to an existing server proxy.") return server def start_server(self): """Start the remote server. Returns ------- ServerProxy Instance of the proxy, if the connection was successful. Raises ------ RPCServerError If the server providing the requested service cannot be reached after 100 contact attempts (*pings*). """ env = compas._os.prepare_environment() try: Popen except NameError: self._process = Process() for name in env: if self._process.StartInfo.EnvironmentVariables.ContainsKey( name): self._process.StartInfo.EnvironmentVariables[name] = env[ name] else: self._process.StartInfo.EnvironmentVariables.Add( name, env[name]) self._process.StartInfo.UseShellExecute = False self._process.StartInfo.RedirectStandardOutput = True self._process.StartInfo.RedirectStandardError = True self._process.StartInfo.FileName = self.python self._process.StartInfo.Arguments = '-m {0} {1}'.format( self.service, str(self._port)) self._process.Start() else: args = [self.python, '-m', self.service, str(self._port)] self._process = Popen(args, stdout=PIPE, stderr=STDOUT, env=env) server = ServerProxy(self.address) print("Starting a new proxy server...") success = False count = 100 while count: try: server.ping() except: time.sleep(0.1) count -= 1 print(" {} attempts left.".format(count)) else: success = True break if not success: raise RPCServerError("The server is not available.") else: print("New proxy server started.") return server def stop_server(self): """Stop the remote server and terminate/kill the python process that was used to start it. """ print("Stopping the server proxy.") try: self._server.remote_shutdown() except: pass self._terminate_process() def _terminate_process(self): """Attempts to terminate the python process hosting the proxy server. The process reference might not be present, e.g. in the case of reusing an existing connection. In that case, this is a no-op. """ if not self._process: return try: self._process.terminate() except: pass try: self._process.kill() except: pass def __getattr__(self, name): if self.package: name = "{}.{}".format(self.package, name) try: self._function = getattr(self._server, name) except: raise RPCServerError() return self.proxy def proxy(self, *args, **kwargs): """Callable replacement for the requested functionality. Parameters ---------- args : list Positional arguments to be passed to the remote function. kwargs : dict Named arguments to be passed to the remote function. Returns ------- object The result returned by the remote function. Warning ------- The `args` and `kwargs` have to be JSON-serialisable. This means that, currently, only native Python objects are supported. The returned results will also always be in the form of built-in Python objects. """ idict = {'args': args, 'kwargs': kwargs} istring = json.dumps(idict, cls=DataEncoder) try: ostring = self._function(istring) except: self.stop_server() raise if not ostring: raise RPCServerError("No output was generated.") result = json.loads(ostring) if result['error']: raise RPCServerError(result['error']) self.profile = result['profile'] return result['data']
class Proxy(object): """Create a proxy object as intermediary between client code and remote functionality. This class is a context manager, so when used in a ``with`` statement, it ensures the remote proxy server is stopped and disposed correctly. However, if the proxy server is left open, it can be re-used for a follow-up connection, saving start up time. Parameters ---------- package : str, optional The base package for the requested functionality. Default is None, in which case a full path to function calls should be provided. python : str, optional The python executable that should be used to execute the code. url : str, optional The server address. port : int, optional The port number on the remote server. service : str, optional Package name to start server. Default is ``'compas.rpc.services.default'``. max_conn_attempts: int, optional Amount of attempts to connect to RPC server before time out. autoreload : bool, optional If True, automatically reload the proxied package if changes are detected. This is particularly useful during development. The server will monitor changes to the files that were loaded as a result of accessing the specified `package` and if any change is detected, it will unload the module, so that the next invocation uses a fresh version. capture_output : bool, optional If True, capture the stdout/stderr output of the remote process. In general, `capture_output` should be True when using a `pythonw` as executable (default). Attributes ---------- address : str, read-only Address of the server as a combination of `url` and `port`. profile : str A profile of the code executed by the server. package : str Fully qualified name of the package or module from where functions should be imported on the server side. service : str Fully qualified package name required for starting the server/service. python : str The type of Python executable that should be used to execute the code. Notes ----- If the server is your *localhost*, which will often be the case, it is better to specify the address explicitly (``'http://127.0.0.1'``) because resolving *localhost* takes a surprisingly significant amount of time. The service will make the correct (version of the requested) functionality available even if that functionality is part of a virtual environment. This is because it will use the specific python interpreter for which the functionality is installed to start the server. If possible, the proxy will try to reconnect to an already existing service Examples -------- Minimal example showing connection to the proxy server, and ensuring the server is disposed after using it: >>> from compas.rpc import Proxy # doctest: +SKIP >>> with Proxy('compas.numerical') as numerical: # doctest: +SKIP ... pass # doctest: +SKIP ... # doctest: +SKIP Starting a new proxy server... # doctest: +SKIP New proxy server started. # doctest: +SKIP Stopping the server proxy. # doctest: +SKIP """ def __init__(self, package=None, python=None, url='http://127.0.0.1', port=1753, service=None, max_conn_attempts=100, autoreload=True, capture_output=True): self._package = None self._python = compas._os.select_python(python) self._url = url self._port = port self.max_conn_attempts = max_conn_attempts self._service = None self._process = None self._function = None self._profile = None self.service = service self.package = package self.autoreload = autoreload self.capture_output = capture_output self._implicitely_started_server = False self._server = self._try_reconnect() if self._server is None: self._server = self.start_server() self._implicitely_started_server = True # ========================================================================== # properties # ========================================================================== @property def address(self): return "{}:{}".format(self._url, self._port) @property def profile(self): return self._profile @profile.setter def profile(self, profile): self._profile = profile @property def package(self): return self._package @package.setter def package(self, package): self._package = package @property def service(self): return self._service @service.setter def service(self, service): if not service: self._service = 'compas.rpc.services.default' else: self._service = service @property def python(self): return self._python @python.setter def python(self, python): self._python = python # ========================================================================== # customization # ========================================================================== def __enter__(self): return self def __exit__(self, *args): # If we started the RPC server, we will try to clean up and stop it # otherwise we just disconnect from it if self._implicitely_started_server: self.stop_server() else: self._server.__close() def __getattr__(self, name): """Find server attributes (methods) corresponding to attributes that do not exist on the proxy itself. 1. Use :attr:`package` as a namespace for the requested attribute to create a fully qualified path on the server. 2. Try to get the fully qualified attribute from :attr:`_server`. 3. If successful, store the result in :attr:`_function`. 3. Return a handle to :meth:`_proxy`, which will delegate calls to :attr:`_function`. Returns ------- callable """ if self.package: name = "{}.{}".format(self.package, name) try: self._function = getattr(self._server, name) except Exception: raise RPCServerError() return self._proxy # ========================================================================== # methods # ========================================================================== def _try_reconnect(self): """Try and reconnect to an existing proxy server. Returns ------- ServerProxy Instance of the proxy if reconnection succeeded, otherwise ``None``. """ server = ServerProxy(self.address) try: server.ping() except Exception: return None else: print("Reconnecting to an existing server proxy.") return server def start_server(self): """Start the remote server. Returns ------- ServerProxy Instance of the proxy, if the connection was successful. Raises ------ RPCServerError If the server providing the requested service cannot be reached after 100 contact attempts (*pings*). The number of attempts is set by :attr:`Proxy.max_conn_attempts`. Examples -------- >>> p = Proxy() # doctest: +SKIP >>> p.stop_server() # doctest: +SKIP >>> p.start_server() # doctest: +SKIP """ env = compas._os.prepare_environment() # this part starts the server side of the RPC setup # it basically launches a subprocess # to start the default service # the default service creates a server # and registers a dispatcher for custom functionality try: Popen except NameError: self._process = Process() for name in env: if self._process.StartInfo.EnvironmentVariables.ContainsKey( name): self._process.StartInfo.EnvironmentVariables[name] = env[ name] else: self._process.StartInfo.EnvironmentVariables.Add( name, env[name]) self._process.StartInfo.UseShellExecute = False self._process.StartInfo.RedirectStandardOutput = self.capture_output self._process.StartInfo.RedirectStandardError = self.capture_output self._process.StartInfo.FileName = self.python self._process.StartInfo.Arguments = '-m {0} --port {1} --{2}autoreload'.format( self.service, self._port, '' if self.autoreload else 'no-') self._process.Start() else: args = [ self.python, '-m', self.service, '--port', str(self._port), '--{}autoreload'.format('' if self.autoreload else 'no-') ] kwargs = dict(env=env) if self.capture_output: kwargs['stdout'] = PIPE kwargs['stderr'] = PIPE self._process = Popen(args, **kwargs) # this starts the client side # it creates a proxy for the server # and tries to connect the proxy to the actual server server = ServerProxy(self.address) print("Starting a new proxy server...") success = False attempt_count = 0 while attempt_count < self.max_conn_attempts: try: server.ping() except Exception: time.sleep(0.1) attempt_count += 1 print(" {} attempts left.".format(self.max_conn_attempts - attempt_count)) else: success = True break if not success: raise RPCServerError("The server is not available.") else: print("New proxy server started.") return server def stop_server(self): """Stop the remote server and terminate/kill the python process that was used to start it. Returns ------- None Examples -------- >>> p = Proxy() # doctest: +SKIP >>> p.stop_server() # doctest: +SKIP >>> p.start_server() # doctest: +SKIP """ print("Stopping the server proxy.") try: self._server.remote_shutdown() except Exception: pass self._terminate_process() def restart_server(self): """Restart the server. Returns ------- None """ self.stop_server() self.start_server() def _terminate_process(self): """Attempts to terminate the python process hosting the proxy server. The process reference might not be present, e.g. in the case of reusing an existing connection. In that case, this is a no-op. """ if not self._process: return try: self._process.terminate() except Exception: pass try: self._process.kill() except Exception: pass def _proxy(self, *args, **kwargs): """Callable replacement for the requested functionality. Parameters ---------- *args : list Positional arguments to be passed to the remote function. **kwargs : dict, optional Named arguments to be passed to the remote function. Returns ------- object The 'data' part of the result dict returned by the remote function. The result dict has the following structure :: { 'error' : ..., # A traceback of the error raised on the server, if any. 'profile' : ..., # A profile of the code executed on the server, if there was no error. 'data' : ..., # The result returned by the target function, if there was no error. } Warnings -------- The `args` and `kwargs` have to be JSON-serializable. This means that, currently, only COMPAS data objects (geometry, robots, data structures) and native Python objects are supported. The returned results will also always be in the form of COMPAS data objects and built-in Python objects. Numpy objects are automatically converted to their built-in Python equivalents. """ idict = {'args': args, 'kwargs': kwargs} istring = json.dumps(idict, cls=DataEncoder) # it makes sense that there is a broken pipe error # because the process is not the one receiving the feedback # when there is a print statement on the server side # this counts as output # it should be sent as part of RPC communication try: ostring = self._function(istring) except Exception: # not clear what the point of this is # self.stop_server() # if this goes wrong, it means a Fault error was generated by the server # no need to stop the server for this raise if not ostring: raise RPCServerError("No output was generated.") result = json.loads(ostring, cls=DataDecoder) if result['error']: raise RPCServerError(result['error']) self.profile = result['profile'] return result['data']
class Proxy(object): """Create a proxy object as intermediary between client code and remote functionality. This class is a context manager, so when used in a ``with`` statement, it ensures the remote proxy server is stopped and disposed correctly. However, if the proxy server is left open, it can be re-used for a follow-up connection, saving start up time. Parameters ---------- package : string, optional The base package for the requested functionality. Default is `None`, in which case a full path to function calls should be provided. python : string, optional The python executable that should be used to execute the code. Default is ``'pythonw'``. url : string, optional The server address. Default is ``'http://127.0.0.1'``. port : int, optional The port number on the remote server. Default is ``1753``. max_conn_attempts: :obj:`int`, optional Amount of attempts to connect to RPC server before time out. Notes ----- If the server is your *localhost*, which will often be the case, it is better to specify the address explicitly (``'http://127.0.0.1'``) because resolving *localhost* takes a surprisingly significant amount of time. The service will make the correct (version of the requested) functionality available even if that functionality is part of a virtual environment. This is because it will use the specific python interpreter for which the functionality is installed to start the server. If possible, the proxy will try to reconnect to an already existing service Examples -------- Minimal example showing connection to the proxy server, and ensuring the server is disposed after using it: .. code-block:: python from compas.rpc import Proxy with Proxy('compas.numerical') as numerical: pass """ def __init__(self, package=None, python=None, url='http://127.0.0.1', port=1753, service=None, max_conn_attempts=100): self._package = None self._python = compas._os.select_python(python) self._url = url self._port = port self.max_conn_attempts = max_conn_attempts self._service = None self._process = None self._function = None self._profile = None self.service = service self.package = package self._implicitely_started_server = False self._server = self._try_reconnect() if self._server is None: self._server = self.start_server() self._implicitely_started_server = True def __enter__(self): return self def __exit__(self, *args): # If we started the RPC server, we will try to clean up and stop it # otherwise we just disconnect from it if self._implicitely_started_server: self.stop_server() else: self._server.__close() @property def address(self): return "{}:{}".format(self._url, self._port) @property def profile(self): """A profile of the executed code.""" return self._profile @profile.setter def profile(self, profile): self._profile = profile @property def package(self): """The base package from which functionality will be called.""" return self._package @package.setter def package(self, package): self._package = package @property def service(self): return self._service @service.setter def service(self, service): if not service: self._service = 'compas.rpc.services.default' else: self._service = service @property def python(self): return self._python @python.setter def python(self, python): self._python = python def _try_reconnect(self): """Try and reconnect to an existing proxy server. Returns ------- ServerProxy Instance of the proxy if reconnection succeeded, otherwise ``None``. """ server = ServerProxy(self.address) try: server.ping() except Exception: return None else: print("Reconnecting to an existing server proxy.") return server def start_server(self): """Start the remote server. Returns ------- ServerProxy Instance of the proxy, if the connection was successful. Raises ------ RPCServerError If the server providing the requested service cannot be reached after 100 contact attempts (*pings*). The number of attempts is set by :attr:`Proxy.max_conn_attempts`. Examples -------- >>> p = Proxy() >>> p.stop_server() >>> p.start_server() """ env = compas._os.prepare_environment() # this part starts the server side of the RPC setup # it basically launches a subprocess # to start the default service # the default service creates a server # and registers a dispatcher for custom functionality try: Popen except NameError: self._process = Process() for name in env: if self._process.StartInfo.EnvironmentVariables.ContainsKey( name): self._process.StartInfo.EnvironmentVariables[name] = env[ name] else: self._process.StartInfo.EnvironmentVariables.Add( name, env[name]) self._process.StartInfo.UseShellExecute = False self._process.StartInfo.RedirectStandardOutput = True self._process.StartInfo.RedirectStandardError = True self._process.StartInfo.FileName = self.python self._process.StartInfo.Arguments = '-m {0} {1}'.format( self.service, str(self._port)) self._process.Start() else: args = [self.python, '-m', self.service, str(self._port)] self._process = Popen(args, stdout=PIPE, stderr=PIPE, env=env) # this starts the client side # it creates a proxy for the server # and tries to connect the proxy to the actual server server = ServerProxy(self.address) print("Starting a new proxy server...") success = False attempt_count = 0 while attempt_count < self.max_conn_attempts: try: server.ping() except Exception: time.sleep(0.1) attempt_count += 1 print(" {} attempts left.".format(self.max_conn_attempts - attempt_count)) else: success = True break if not success: raise RPCServerError("The server is not available.") else: print("New proxy server started.") return server def stop_server(self): """Stop the remote server and terminate/kill the python process that was used to start it. Examples -------- >>> p = Proxy() >>> p.stop_server() >>> p.start_server() """ print("Stopping the server proxy.") try: self._server.remote_shutdown() except Exception: pass self._terminate_process() def _terminate_process(self): """Attempts to terminate the python process hosting the proxy server. The process reference might not be present, e.g. in the case of reusing an existing connection. In that case, this is a no-op. """ if not self._process: return try: self._process.terminate() except Exception: pass try: self._process.kill() except Exception: pass def __getattr__(self, name): if self.package: name = "{}.{}".format(self.package, name) try: self._function = getattr(self._server, name) except Exception: raise RPCServerError() return self._proxy def _proxy(self, *args, **kwargs): """Callable replacement for the requested functionality. Parameters ---------- args : list Positional arguments to be passed to the remote function. kwargs : dict Named arguments to be passed to the remote function. Returns ------- object The result returned by the remote function. Warnings -------- The `args` and `kwargs` have to be JSON-serialisable. This means that, currently, only native Python objects are supported. The returned results will also always be in the form of built-in Python objects. """ idict = {'args': args, 'kwargs': kwargs} istring = json.dumps(idict, cls=DataEncoder) # it makes sense that there is a broken pipe error # because the process is not the one receiving the feedback # when there is a print statement on the server side # this counts as output # it should be sent as part of RPC communication try: ostring = self._function(istring) except Exception: # not clear what the point of this is # self.stop_server() # if this goes wrong, it means a Fault error was generated by the server # no need to stop the server for this raise if not ostring: raise RPCServerError("No output was generated.") result = json.loads(ostring) if result['error']: raise RPCServerError(result['error']) self.profile = result['profile'] return result['data']