diff --git a/cura/BackendPlugin.py b/cura/BackendPlugin.py new file mode 100644 index 0000000000..6493f20487 --- /dev/null +++ b/cura/BackendPlugin.py @@ -0,0 +1,74 @@ +# Copyright (c) 2023 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import subprocess +from typing import Optional, List + +from UM.Logger import Logger +from UM.PluginObject import PluginObject + + +class BackendPlugin(PluginObject): + def __init__(self) -> None: + super().__init__() + self.__port: int = 0 + self._plugin_address: str = "127.0.0.1" + self._plugin_command: Optional[List[str]] = None + self._process = None + self._is_running = False + + def isRunning(self): + return self._is_running + + def setPort(self, port: int) -> None: + self.__port = port + + def getPort(self) -> int: + return self.__port + + def _validatePluginCommand(self) -> list[str]: + """ + Validate the plugin command and add the port parameter if it is missing. + + :return: A list of strings containing the validated plugin command. + """ + if not self._plugin_command or "--port" in self._plugin_command: + return self._plugin_command or [] + + return self._plugin_command + ["--port", str(self.__port)] + + def start(self) -> bool: + """ + Starts the backend_plugin process. + + :return: True if the plugin process started successfully, False otherwise. + """ + try: + # STDIN needs to be None because we provide no input, but communicate via a local socket instead. + # The NUL device sometimes doesn't exist on some computers. + self._process = subprocess.Popen(self._validatePluginCommand(), stdin = None) + self._is_running = True + return True + except PermissionError: + Logger.log("e", f"Couldn't start backend_plugin [{self._plugin_id}]: No permission to execute process.") + except FileNotFoundError: + Logger.logException("e", f"Unable to find backend_plugin executable [{self._plugin_id}]") + except BlockingIOError: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Resource is temporarily unavailable") + except OSError as e: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Operating system is blocking it (antivirus?)") + return False + + def stop(self) -> bool: + if not self._process: + self._is_running = False + return True # Nothing to stop + + try: + self._process.terminate() + return_code = self._process.wait() + self._is_running = False + Logger.log("d", f"Backend_plugin [{self._plugin_id}] was killed. Received return code {return_code}") + return True + except PermissionError: + Logger.log("e", "Unable to kill running engine. Access is denied.") + return False diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6b04503ebc..c96de7e50a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -205,6 +205,8 @@ class CuraApplication(QtApplication): self._cura_scene_controller = None self._machine_error_checker = None + self._backend_plugins: List[BackendPlugin] = [] + self._machine_settings_manager = MachineSettingsManager(self, parent = self) self._material_management_model = None self._quality_management_model = None @@ -792,6 +794,7 @@ class CuraApplication(QtApplication): self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_writer", self._addProfileWriter) + self._plugin_registry.addType("backend_plugin", self._addBackendPlugin) if Platform.isLinux(): lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions. @@ -1730,6 +1733,12 @@ class CuraApplication(QtApplication): def _addProfileWriter(self, profile_writer): pass + def _addBackendPlugin(self, backend_plugin: "BackendPlugin") -> None: + self._backend_plugins.append(backend_plugin) + + def getBackendPlugins(self) -> List["BackendPlugin"]: + return self._backend_plugins + @pyqtSlot("QSize") def setMinimumWindowSize(self, size): main_window = self.getMainWindow() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index f5d701f6f7..e53d29d9b9 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -70,7 +70,7 @@ class CuraEngineBackend(QObject, Backend): os.path.join(CuraApplication.getInstallPrefix(), "bin"), os.path.dirname(os.path.abspath(sys.executable)), ] - + self._last_backend_plugin_port = self._port + 1000 for path in search_path: engine_path = os.path.join(path, executable_name) if os.path.isfile(engine_path): @@ -176,6 +176,20 @@ class CuraEngineBackend(QObject, Backend): application.initializationFinished.connect(self.initialize) + def startPlugins(self) -> None: + """ + Ensure that all backend plugins are started + :return: + """ + backend_plugins = CuraApplication.getInstance().getBackendPlugins() + for backend_plugin in backend_plugins: + if backend_plugin.isRunning(): + continue + # Set the port to prevent plugins from using the same one. + backend_plugin.setPort(self._last_backend_plugin_port) + self.__last_backend_plugin_port += 1 + backend_plugin.start() + def _resetLastSliceTimeStats(self) -> None: self._time_start_process = None self._time_send_message = None