Browse Source

Move backend plugin logic to Cura from Uranium

I was running into abstraction issues when it was defined in Uranium.
Instead of trying to fight those, it's just easier to move it to Cura

CURA-10717
Jaime van Kessel 1 year ago
parent
commit
f3bc7bf28a
3 changed files with 98 additions and 1 deletions
  1. 74 0
      cura/BackendPlugin.py
  2. 9 0
      cura/CuraApplication.py
  3. 15 1
      plugins/CuraEngineBackend/CuraEngineBackend.py

+ 74 - 0
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

+ 9 - 0
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()

+ 15 - 1
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