diff --git a/pyproject.toml b/pyproject.toml index ab2ad0931..a2af4a616 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "psutil >= 5.9.0", "requests >= 2.27.1", "packaging >= 21.3", + "jsonrpcserver >= 5.0.9" ] [project.optional-dependencies] diff --git a/src/ansys/motorcad/core/__init__.py b/src/ansys/motorcad/core/__init__.py index b563354d0..917b538c4 100644 --- a/src/ansys/motorcad/core/__init__.py +++ b/src/ansys/motorcad/core/__init__.py @@ -9,4 +9,8 @@ set_default_instance, set_motorcad_exe, set_server_ip, + how_many_open, + IS_REMOTE_MACHINE, + RemoteMachine, + add_remote_machine ) diff --git a/src/ansys/motorcad/core/instance_manager/rpc_server.py b/src/ansys/motorcad/core/instance_manager/rpc_server.py new file mode 100644 index 000000000..98cc7fe93 --- /dev/null +++ b/src/ansys/motorcad/core/instance_manager/rpc_server.py @@ -0,0 +1,111 @@ +import psutil + +import ansys.motorcad.core as pymotorcad +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from jsonrpcserver import Success, method, Error, dispatch + + +import threading + + +MAX_INSTANCES = 4 +PORT = 34000 + + +num_instances = 0 +motor_launch_lock = threading.Lock() + +motor_instances = [] + +motor_instant_lock = threading.Lock() + + +def get_mc_from_port(a_port): + for mc in motor_instances: + if mc.connection._port == a_port: + return mc + + +def remove_mc_from_list(a_port): + for mc in motor_instances: + if mc.connection._port == a_port: + motor_instances.remove(mc) + return + + +@method +def send_command_remote(a_port, a_method, a_params): + mc = get_mc_from_port(a_port) + if mc is not None: + result = mc.connection.send_and_receive(a_method, a_params) + return Success(result) + else: + return Error(1) + + +@method +def close_motor_cad(aPort): + + print(str(aPort) + ": attempting to close") + + try: + mc = get_mc_from_port(aPort) + mc.quit() + + print(str(aPort) + ": closed successfully ") + result = Success() + except: + print(str(aPort) + ": failed to close") + result = Error(1, "failed to close") + finally: + remove_mc_from_list(aPort) + return result + + +@method +def open_motor_cad(): + + motor_launch_lock.acquire() + + try: + if pymotorcad.how_many_open() < MAX_INSTANCES: + mc_object = pymotorcad.MotorCAD() # , Port=freePort) + print("started MotorCAD") + + else: + print("server is full") + return Success(-1) + + finally: + motor_launch_lock.release() + + mc_object.connection._wait_for_server_to_start(psutil.Process(mc_object.connection.pid)) + mc_object.connection._wait_for_response(30) + + port = mc_object.connection._port + + motor_instant_lock.acquire() + try: + motor_instances.append(mc_object) + finally: + motor_instant_lock.release() + + return Success(port) + + +class TestHttpServer(BaseHTTPRequestHandler): + def do_POST(self): + # Process request + request = self.rfile.read(int(self.headers["Content-Length"])).decode() + response = dispatch(request) + # Return response + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(response.encode()) + + +if __name__ == "__main__": + pymotorcad.rpc_client_core.IS_REMOTE_MACHINE = True + print("Starting RPC server on port: " + str(PORT)) + ThreadingHTTPServer(("", PORT), TestHttpServer).serve_forever() diff --git a/src/ansys/motorcad/core/motorcad_methods.py b/src/ansys/motorcad/core/motorcad_methods.py index 0851946d6..4fa3aee06 100644 --- a/src/ansys/motorcad/core/motorcad_methods.py +++ b/src/ansys/motorcad/core/motorcad_methods.py @@ -17,6 +17,7 @@ def __init__( enable_exceptions=True, enable_success_variable=False, reuse_parallel_instances=False, + use_remote_machine=False ): self.connection = _MotorCADConnection( port, @@ -24,6 +25,7 @@ def __init__( enable_exceptions, enable_success_variable, reuse_parallel_instances, + use_remote_machine ) _RpcMethodsCore.__init__(self, mc_connection=self.connection) @@ -59,6 +61,7 @@ def __init__( enable_exceptions=True, enable_success_variable=False, reuse_parallel_instances=False, + use_remote_machine=False ): """Initiate MotorCAD object.""" _MotorCADCore.__init__( @@ -68,6 +71,7 @@ def __init__( enable_exceptions=enable_exceptions, enable_success_variable=enable_success_variable, reuse_parallel_instances=reuse_parallel_instances, + use_remote_machine=use_remote_machine ) diff --git a/src/ansys/motorcad/core/rpc_client_core.py b/src/ansys/motorcad/core/rpc_client_core.py index 8a1ebf672..c3a70d96b 100644 --- a/src/ansys/motorcad/core/rpc_client_core.py +++ b/src/ansys/motorcad/core/rpc_client_core.py @@ -22,6 +22,35 @@ MOTORCAD_PROC_NAMES = ["MotorCAD", "Motor-CAD"] +IS_REMOTE_MACHINE = False +REMOTE_MACHINE_LIST = [] + + +class RemoteMachine: + def __init__( + self, + max_connections=4, + server_ip="localhost", + server_port=34000, + ): + self.max_connections = max_connections + + # if server_ip == "localhost": + # # Don't want to resolve localhost for every command + # hostname = socket.gethostname() + # self._server_ip = socket.gethostbyname(hostname) + # else: + + self._server_ip = server_ip + + self.serverPort = server_port + + self.server_url = "http://" + str(self._server_ip) + ":" + str(server_port) + + +def add_remote_machine(remote_machine): + REMOTE_MACHINE_LIST.append(remote_machine) + def set_server_ip(ip): """IP address of the machine that Motor-CAD is running on.""" @@ -117,10 +146,17 @@ def _find_motor_cad_exe(): except Exception: raise MotorCADError("Error reading Motor-CAD batch file. " + str_alt_method) +def how_many_open(): + num_instances = 0 + for i in psutil.process_iter(): + if (i.name()) in MOTORCAD_PROC_NAMES: + num_instances = num_instances + 1 + + return num_instances + class _MotorCADConnection: """Provides the Motor-CAD instance attached to each MotorCAD object.""" - def __init__( self, port, @@ -128,7 +164,8 @@ def __init__( enable_exceptions, enable_success_variable, reuse_parallel_instances, - compatibility_mode=False, + use_remote_machine, + compatibility_mode=False ): """Create a MotorCAD object for communication. @@ -157,14 +194,13 @@ def __init__( self._last_error_message = "" self.program_version = "" self.pid = -1 - self.enable_exceptions = enable_exceptions self.reuse_parallel_instances = reuse_parallel_instances self._open_new_instance = open_new_instance self.enable_success_variable = enable_success_variable - + self.use_remote_machine = use_remote_machine self._compatibility_mode = compatibility_mode if DEFAULT_INSTANCE != -1: @@ -175,7 +211,15 @@ def __init__( if self.reuse_parallel_instances is True: self._open_new_instance = False - if self._open_new_instance is True: + if (IS_REMOTE_MACHINE is True) and (open_new_instance is True): + self._open_motor_cad_local() + return # call finish_connection separately + + if self.use_remote_machine is True: + self.__open_motor_cad_remote() + waitTime = 30 + + elif self._open_new_instance is True: if port != -1: self._port = int(port) @@ -188,6 +232,9 @@ def __init__( else: # port is not defined self._find_free_motor_cad() + self.finish_connection() + + def finish_connection(self): self.pid = self.get_process_id() # Check for response @@ -208,7 +255,13 @@ def __init__( def __del__(self): """Close Motor-CAD when MotorCAD object leaves memory.""" if self._connected is True: - if ( + if self.use_remote_machine is True: + try: + self._send_command_remote_machine("close_motor_cad", aParams=[self._port]) + except Exception: + # Don't worry about this - might already be closed + pass + elif ( (self.reuse_parallel_instances is False) and (self._open_new_instance is True) and (self._compatibility_mode is False) @@ -245,12 +298,46 @@ def _open_motor_cad_local(self): [self.__MotorExe, "/PORT=" + str(self._port), "/SCRIPTING"] ) - pid = motor_process.pid + self.pid = motor_process.pid + + motor_util = psutil.Process(self.pid) + + if IS_REMOTE_MACHINE is False: + self._wait_for_server_to_start(motor_util) - motor_util = psutil.Process(pid) + def __open_motor_cad_remote(self): + global SERVER_IP + for remote_machine in REMOTE_MACHINE_LIST: + self._port = self._send_command_remote_machine( + "open_motor_cad", remoteMachineUrl=remote_machine.server_url) - self._wait_for_server_to_start(motor_util) + if self._port != -1: # Returns -1 if failed to start + SERVER_IP = "http://" + remote_machine._server_ip + self._RemoteMachineUrl = remote_machine.server_url + self._url = self._get_url() + break + else: + raise MotorCADError("Failed to find machine to launch MotorCAD") + + def _send_command_remote_machine(self, aCommand, aParams=[], remoteMachineUrl=""): + try: + if not remoteMachineUrl: + remoteMachineUrl = self._RemoteMachineUrl + + JSON_command = { + "jsonrpc": "2.0", + "method": aCommand, + "params": aParams, + "id": 1, # Can be any number not just linked to port + } + response = requests.post(remoteMachineUrl, json=JSON_command).json() + + return response["result"] + except Exception as e: + strE = str(e) + + def _find_free_motor_cad(self): found_free_instance = False for proc in psutil.process_iter(): @@ -323,11 +410,20 @@ def send_and_receive(self, method, params=None, success_var=None): try: # Special case as there won't be a response - if method == "Quit": - requests.post(self._get_url(), json=payload).json() - return + if self.use_remote_machine is True: + remote_params = [self._port, method, params] + if method == "Quit": + self._send_command_remote_machine("send_command_remote", remote_params) + return + else: + response = self._send_command_remote_machine("send_command_remote", remote_params) else: - response = requests.post(self._get_url(), json=payload).json() + + if method == "Quit": + requests.post(self._get_url(), json=payload).json() + return + else: + response = requests.post(self._get_url(), json=payload).json() except Exception as e: # This can occur when an assert fails in Motor-CAD debug @@ -335,74 +431,80 @@ def send_and_receive(self, method, params=None, success_var=None): self._raise_if_allowed("RPC Communication failed: " + str(e)) else: # No exceptions in RPC communication - if "error" in response: - error_string = "RPC Communication Error: " + response["error"]["message"] - - if "Invalid params" in error_string: - try: - # common error - give a better error message - new_error_string = error_string.split("hint") - # Get last part - new_error_string = new_error_string[-1] - - new_error_string = ( - method - + ": One or more parameter types were invalid. HINT [" - + new_error_string - ) - error_string = new_error_string - except Exception: - # use old error string if that failed - pass - - success = -99 - self._last_error_message = error_string - - self._raise_if_allowed(error_string) - return - + if IS_REMOTE_MACHINE: + return response else: - success = response["result"]["success"] + return self._handle_response(response, method, success_var) - if method == "CheckIfGeometryIsValid": - # This doesn't have the normal success var - success_value = 1 - else: - success_value = _METHOD_SUCCESS - - if success != success_value: - # This is an error caused by bad user code - # Exception is enabled by default - # Can get error message (get_last_error_message) instead - if response["result"]["errorMessage"] != "": - error_message = response["result"]["errorMessage"] - else: - error_message = ( - "An error occurred in Motor-CAD." # put some generic error message + def _handle_response(self, response, method, success_var): + if "error" in response: + error_string = "RPC Communication Error: " + response["error"]["message"] + + if "Invalid params" in error_string: + try: + # common error - give a better error message + new_error_string = error_string.split("hint") + # Get last part + new_error_string = new_error_string[-1] + + new_error_string = ( + method + + ": One or more parameter types were invalid. HINT [" + + new_error_string ) + error_string = new_error_string + except Exception: + # use old error string if that failed + pass + + success = -99 + self._last_error_message = error_string + + self._raise_if_allowed(error_string) + return + + else: + success = response["result"]["success"] - self._last_error_message = error_message + if method == "CheckIfGeometryIsValid": + # This doesn't have the normal success var + success_value = 1 + else: + success_value = _METHOD_SUCCESS + + if success != success_value: + # This is an error caused by bad user code + # Exception is enabled by default + # Can get error message (get_last_error_message) instead + if response["result"]["errorMessage"] != "": + error_message = response["result"]["errorMessage"] + else: + error_message = ( + "An error occurred in Motor-CAD." # put some generic error message + ) - self._raise_if_allowed(error_message) + self._last_error_message = error_message - result_list = [] + self._raise_if_allowed(error_message) - if success_var is None: - success_var = self.enable_success_variable + result_list = [] - if success_var is True: - result_list.append(success) + if success_var is None: + success_var = self.enable_success_variable - if len(response["result"]["output"]) > 0: - if len(response["result"]["output"]) == 1: - result_list.append(response["result"]["output"][0]) - else: - result_list.extend(list(response["result"]["output"])) + if success_var is True: + result_list.append(success) + + if len(response["result"]["output"]) > 0: + if len(response["result"]["output"]) == 1: + result_list.append(response["result"]["output"][0]) + else: + result_list.extend(list(response["result"]["output"])) - if len(result_list) > 1: - return tuple(result_list) - elif len(result_list) == 1: - return result_list[0] + if len(result_list) > 1: + return tuple(result_list) + elif len(result_list) == 1: + return result_list[0] def _wait_for_response(self, max_retries): method = "Handshake"