From 0e306df1bcc0c0e6126530a28a498073be85898c Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 2 Feb 2017 15:59:09 +0100 Subject: [PATCH 1/2] Initial basic version of this feature. CURA-3335 --- cura/CuraApplication.py | 35 +++++++++++++++++++++++++++++++++++ cura_app.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index df3f44c14f..7b566c17ac 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,5 +1,9 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. +import json + +from PyQt5.QtCore import QTextStream +from PyQt5.QtNetwork import QLocalServer from UM.Qt.QtApplication import QtApplication from UM.Scene.SceneNode import SceneNode @@ -420,13 +424,44 @@ 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) + + 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('d', 'Saw something on the single instance server') + other_cura_connection = self.__single_instance_server.nextPendingConnection() + if other_cura_connection is not None: + def readyRead(): + while other_cura_connection.canReadLine(): + line = other_cura_connection.readLine() + payload = json.loads(str(line, encoding="ASCII").strip()) + command = payload["command"] + if command == "clear-all": + self.deleteAll() + + elif command == "open": + self.deleteAll() + self._openFile(payload["filePath"]) + + elif command == "focus": + self.focusWindow() + + other_cura_connection.readyRead.connect(readyRead) 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..c2ee6a72b1 100755 --- a/cura_app.py +++ b/cura_app.py @@ -2,11 +2,14 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. - +import argparse +import json import os import sys import platform +import time +from PyQt5.QtNetwork import QLocalSocket from UM.Platform import Platform #WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612 @@ -58,5 +61,36 @@ if Platform.isWindows() and hasattr(sys, "frozen"): # Force an instance of CuraContainerRegistry to be created and reused later. cura.Settings.CuraContainerRegistry.getInstance() +# Peek the arguments and look for the 'single-instance' flag. +parser = argparse.ArgumentParser(prog="cura") # pylint: disable=bad-whitespace +cura.CuraApplication.CuraApplication.addCommandLineOptions(parser) +parsed_command_line = vars(parser.parse_args()) + +if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]: + print("Check for single instance") + single_instance_socket = QLocalSocket() + single_instance_socket.connectToServer("ultimaker-cura") + single_instance_socket.waitForConnected() + if single_instance_socket.state() == QLocalSocket.ConnectedState: + print("Connected to the other Cura instance.") + print(repr(parsed_command_line)) + + 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() + sys.exit(0) + app = cura.CuraApplication.CuraApplication.getInstance() app.run() From 412e299f0cd316b7b3b4132878ae5f9059d782e1 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 7 Feb 2017 13:33:37 +0100 Subject: [PATCH 2/2] Cleaned up and bug fixed the command loop. CURA-3335 Single instance Cura and model reloading --- cura/CuraApplication.py | 94 ++++++++++++++++++++++++++++++++--------- cura_app.py | 37 ++-------------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7b566c17ac..a797cc2966 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,9 +1,7 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -import json - -from PyQt5.QtCore import QTextStream from PyQt5.QtNetwork import QLocalServer +from PyQt5.QtNetwork import QLocalSocket from UM.Qt.QtApplication import QtApplication from UM.Scene.SceneNode import SceneNode @@ -63,7 +61,8 @@ import numpy import copy import urllib.parse import os - +import argparse +import json numpy.seterr(all="ignore") @@ -430,6 +429,7 @@ class CuraApplication(QtApplication): 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() @@ -437,25 +437,79 @@ class CuraApplication(QtApplication): self.__single_instance_server.listen("ultimaker-cura") def _singleInstanceServerNewConnection(self): - Logger.log('d', 'Saw something on the single instance server') - other_cura_connection = self.__single_instance_server.nextPendingConnection() - if other_cura_connection is not None: - def readyRead(): - while other_cura_connection.canReadLine(): - line = other_cura_connection.readLine() - payload = json.loads(str(line, encoding="ASCII").strip()) - command = payload["command"] - if command == "clear-all": - self.deleteAll() + Logger.log("i", "New connection recevied on our single-instance server") + remote_cura_connection = self.__single_instance_server.nextPendingConnection() - elif command == "open": - self.deleteAll() - self._openFile(payload["filePath"]) + 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"] - elif command == "focus": - self.focusWindow() + # Command: Remove all models from the build plate. + if command == "clear-all": + self.deleteAll() - other_cura_connection.readyRead.connect(readyRead) + # 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...")) diff --git a/cura_app.py b/cura_app.py index c2ee6a72b1..653f56d34d 100755 --- a/cura_app.py +++ b/cura_app.py @@ -2,14 +2,10 @@ # Copyright (c) 2015 Ultimaker B.V. # Cura is released under the terms of the AGPLv3 or higher. -import argparse -import json import os import sys import platform -import time -from PyQt5.QtNetwork import QLocalSocket from UM.Platform import Platform #WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612 @@ -61,36 +57,9 @@ if Platform.isWindows() and hasattr(sys, "frozen"): # Force an instance of CuraContainerRegistry to be created and reused later. cura.Settings.CuraContainerRegistry.getInstance() -# Peek the arguments and look for the 'single-instance' flag. -parser = argparse.ArgumentParser(prog="cura") # pylint: disable=bad-whitespace -cura.CuraApplication.CuraApplication.addCommandLineOptions(parser) -parsed_command_line = vars(parser.parse_args()) - -if "single_instance" in parsed_command_line and parsed_command_line["single_instance"]: - print("Check for single instance") - single_instance_socket = QLocalSocket() - single_instance_socket.connectToServer("ultimaker-cura") - single_instance_socket.waitForConnected() - if single_instance_socket.state() == QLocalSocket.ConnectedState: - print("Connected to the other Cura instance.") - print(repr(parsed_command_line)) - - 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() - sys.exit(0) +# 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()