BackendPlugin.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. # Copyright (c) 2023 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import socket
  4. import os
  5. import subprocess
  6. import pathlib
  7. from typing import Optional, List
  8. from UM.Logger import Logger
  9. from UM.Message import Message
  10. from UM.Settings.AdditionalSettingDefinitionsAppender import AdditionalSettingDefinitionsAppender
  11. from UM.PluginObject import PluginObject
  12. from UM.i18n import i18nCatalog
  13. from UM.Platform import Platform
  14. from UM.Resources import Resources
  15. class BackendPlugin(AdditionalSettingDefinitionsAppender, PluginObject):
  16. catalog = i18nCatalog("cura")
  17. settings_catalog = i18nCatalog("fdmprinter.def.json")
  18. def __init__(self, catalog_i18n = settings_catalog) -> None:
  19. super().__init__(catalog_i18n)
  20. self.__port: int = 0
  21. self._plugin_address: str = "127.0.0.1"
  22. self._plugin_command: Optional[List[str]] = None
  23. self._process = None
  24. self._is_running = False
  25. self._supported_slots: List[int] = []
  26. self._use_plugin = True
  27. def usePlugin(self) -> bool:
  28. return self._use_plugin
  29. def getSupportedSlots(self) -> List[int]:
  30. return self._supported_slots
  31. def isRunning(self):
  32. return self._is_running
  33. def setPort(self, port: int) -> None:
  34. self.__port = port
  35. def getPort(self) -> int:
  36. return self.__port
  37. def getAddress(self) -> str:
  38. return self._plugin_address
  39. def setAvailablePort(self) -> None:
  40. """
  41. Sets the port to a random available port.
  42. """
  43. sock = socket.socket()
  44. sock.bind((self.getAddress(), 0))
  45. port = sock.getsockname()[1]
  46. self.setPort(port)
  47. def _validatePluginCommand(self) -> list[str]:
  48. """
  49. Validate the plugin command and add the port parameter if it is missing.
  50. :return: A list of strings containing the validated plugin command.
  51. """
  52. if not self._plugin_command or "--port" in self._plugin_command:
  53. return self._plugin_command or []
  54. return self._plugin_command + ["--address", self.getAddress(), "--port", str(self.__port)]
  55. def start(self) -> bool:
  56. """
  57. Starts the backend_plugin process.
  58. :return: True if the plugin process started successfully, False otherwise.
  59. """
  60. if not self.usePlugin():
  61. return False
  62. validated_plugin_command = self._validatePluginCommand()
  63. Logger.info(f"Starting backend_plugin [{self._plugin_id}] with command: {validated_plugin_command}")
  64. plugin_log_path = os.path.join(Resources.getDataStoragePath(), f"{self.getPluginId()}.log")
  65. if os.path.exists(plugin_log_path):
  66. try:
  67. os.remove(plugin_log_path)
  68. except:
  69. pass # removing is only done such that it doesn't grow out of proportions, if it fails once or twice that is okay
  70. Logger.info(f"Logging plugin output to: {plugin_log_path}")
  71. try:
  72. # STDIN needs to be None because we provide no input, but communicate via a local socket instead.
  73. # The NUL device sometimes doesn't exist on some computers.
  74. with open(plugin_log_path, 'a') as f:
  75. popen_kwargs = {
  76. "stdin": None,
  77. "stdout": f, # Redirect output to file
  78. "stderr": subprocess.STDOUT, # Combine stderr and stdout
  79. }
  80. if Platform.isWindows():
  81. popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
  82. plugin_env = os.environ
  83. if Platform.isLinux() and len(validated_plugin_command) > 0:
  84. # Add plugin directory to AppImage "modules" directory so that it is started as if it was
  85. # part of the AppImage and uses the sames libraries
  86. plugin_env["APPDIR_MODULE_DIR"] = str(pathlib.Path(validated_plugin_command[0]).parent.absolute())
  87. self._process = subprocess.Popen(validated_plugin_command, env=plugin_env, **popen_kwargs)
  88. self._is_running = True
  89. return True
  90. except PermissionError:
  91. Logger.log("e", f"Couldn't start EnginePlugin: {self._plugin_id} No permission to execute process.")
  92. self._showMessage(self.catalog.i18nc("@info:plugin_failed",
  93. f"Couldn't start EnginePlugin: {self._plugin_id}\nNo permission to execute process."),
  94. message_type = Message.MessageType.ERROR)
  95. except FileNotFoundError:
  96. Logger.logException("e", f"Unable to find local EnginePlugin server executable for: {self._plugin_id}")
  97. self._showMessage(self.catalog.i18nc("@info:plugin_failed",
  98. f"Unable to find local EnginePlugin server executable for: {self._plugin_id}"),
  99. message_type = Message.MessageType.ERROR)
  100. except BlockingIOError:
  101. Logger.logException("e", f"Couldn't start EnginePlugin: {self._plugin_id} Resource is temporarily unavailable")
  102. self._showMessage(self.catalog.i18nc("@info:plugin_failed",
  103. f"Couldn't start EnginePlugin: {self._plugin_id}\nResource is temporarily unavailable"),
  104. message_type = Message.MessageType.ERROR)
  105. except OSError as e:
  106. Logger.logException("e", f"Couldn't start EnginePlugin {self._plugin_id} Operating system is blocking it (antivirus?)")
  107. self._showMessage(self.catalog.i18nc("@info:plugin_failed",
  108. f"Couldn't start EnginePlugin: {self._plugin_id}\nOperating system is blocking it (antivirus?)"),
  109. message_type = Message.MessageType.ERROR)
  110. return False
  111. def stop(self) -> bool:
  112. if not self._process:
  113. self._is_running = False
  114. return True # Nothing to stop
  115. try:
  116. self._process.terminate()
  117. return_code = self._process.wait()
  118. self._is_running = False
  119. Logger.log("d", f"EnginePlugin: {self._plugin_id} was killed. Received return code {return_code}")
  120. return True
  121. except PermissionError:
  122. Logger.log("e", f"Unable to kill running EnginePlugin: {self._plugin_id} Access is denied.")
  123. self._showMessage(self.catalog.i18nc("@info:plugin_failed",
  124. f"Unable to kill running EnginePlugin: {self._plugin_id}\nAccess is denied."),
  125. message_type = Message.MessageType.ERROR)
  126. return False
  127. def _showMessage(self, message: str, message_type: Message.MessageType = Message.MessageType.ERROR) -> None:
  128. Message(message, title=self.catalog.i18nc("@info:title", "EnginePlugin"), message_type = message_type).show()