diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index df3f44c14f..a797cc2966 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,5 +1,7 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. +from PyQt5.QtNetwork import QLocalServer +from PyQt5.QtNetwork import QLocalSocket from UM.Qt.QtApplication import QtApplication from UM.Scene.SceneNode import SceneNode @@ -59,7 +61,8 @@ import numpy import copy import urllib.parse import os - +import argparse +import json numpy.seterr(all="ignore") @@ -420,13 +423,99 @@ class CuraApplication(QtApplication): self._plugins_loaded = True + @classmethod def addCommandLineOptions(self, parser): super().addCommandLineOptions(parser) parser.add_argument("file", nargs="*", help="Files to load after starting the application.") + parser.add_argument("--single-instance", action="store_true", default=False) + + # Set up a local socket server which listener which coordinates single instances Curas and accepts commands. + def _setUpSingleInstanceServer(self): + if self.getCommandLineOption("single_instance", False): + self.__single_instance_server = QLocalServer() + self.__single_instance_server.newConnection.connect(self._singleInstanceServerNewConnection) + self.__single_instance_server.listen("ultimaker-cura") + + def _singleInstanceServerNewConnection(self): + Logger.log("i", "New connection recevied on our single-instance server") + remote_cura_connection = self.__single_instance_server.nextPendingConnection() + + if remote_cura_connection is not None: + def readCommands(): + line = remote_cura_connection.readLine() + while len(line) != 0: # There is also a .canReadLine() + try: + Logger.log('d', "JSON command: " + str(line, encoding="ASCII")) + payload = json.loads(str(line, encoding="ASCII").strip()) + command = payload["command"] + + # Command: Remove all models from the build plate. + if command == "clear-all": + self.deleteAll() + + # Command: Load a model file + elif command == "open": + self._openFile(payload["filePath"]) + # FIXME ^ this method is async and we really should wait until + # the file load is complete before processing more commands. + + # Command: Activate the window and bring it to the top. + elif command == "focus": + self.getMainWindow().raise_() + self.focusWindow() + + else: + Logger.log("w", "Received an unrecognized command " + str(command)) + except json.decoder.JSONDecodeError as ex: + Logger.log("w", "Unable to parse JSON command in _singleInstanceServerNewConnection(): " + repr(ex)) + line = remote_cura_connection.readLine() + + remote_cura_connection.readyRead.connect(readCommands) + remote_cura_connection.disconnected.connect(readCommands) # Get any last commands before it is destroyed. + + ## Perform any checks before creating the main application. + # + # This should be called directly before creating an instance of CuraApplication. + # \returns \type{bool} True if the whole Cura app should continue running. + @classmethod + def preStartUp(cls): + # Peek the arguments and look for the 'single-instance' flag. + parser = argparse.ArgumentParser(prog="cura") # pylint: disable=bad-whitespace + CuraApplication.addCommandLineOptions(parser) + parsed_command_line = vars(parser.parse_args()) + + if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]: + Logger.log("i", "Checking for the presence of an ready running Cura instance.") + single_instance_socket = QLocalSocket() + single_instance_socket.connectToServer("ultimaker-cura") + single_instance_socket.waitForConnected() + if single_instance_socket.state() == QLocalSocket.ConnectedState: + Logger.log("i", "Connection has been made to the single-instance Cura socket.") + + # Protocol is one line of JSON terminated with a carriage return. + # "command" field is required and holds the name of the command to execute. + # Other fields depend on the command. + + payload = {"command": "clear-all"} + single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII")) + + payload = {"command": "focus"} + single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII")) + + if len(parsed_command_line["file"]) != 0: + for filename in parsed_command_line["file"]: + payload = {"command": "open", "filePath": filename} + single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ASCII")) + single_instance_socket.flush() + single_instance_socket.close() + return False + return True def run(self): self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) + self._setUpSingleInstanceServer() + controller = self.getController() controller.setActiveView("SolidView") diff --git a/cura_app.py b/cura_app.py index 5c3ea811b5..653f56d34d 100755 --- a/cura_app.py +++ b/cura_app.py @@ -2,7 +2,6 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. - import os import sys import platform @@ -58,5 +57,9 @@ if Platform.isWindows() and hasattr(sys, "frozen"): # Force an instance of CuraContainerRegistry to be created and reused later. cura.Settings.CuraContainerRegistry.getInstance() +# This prestart up check is needed to determine if we should start the application at all. +if not cura.CuraApplication.CuraApplication.preStartUp(): + sys.exit(0) + app = cura.CuraApplication.CuraApplication.getInstance() app.run()