SingleInstance.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. # Copyright (c) 2018 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import json
  4. import os
  5. from typing import List, Optional
  6. from PyQt5.QtNetwork import QLocalServer, QLocalSocket
  7. from UM.Logger import Logger
  8. class SingleInstance:
  9. def __init__(self, application, files_to_open: Optional[List[str]]):
  10. self._application = application
  11. self._files_to_open = files_to_open
  12. self._single_instance_server = None
  13. # Starts a client that checks for a single instance server and sends the files that need to opened if the server
  14. # exists. Returns True if the single instance server is found, otherwise False.
  15. def startClient(self) -> bool:
  16. Logger.log("i", "Checking for the presence of an ready running Cura instance.")
  17. single_instance_socket = QLocalSocket(self._application)
  18. Logger.log("d", "Full single instance server name: %s", single_instance_socket.fullServerName())
  19. single_instance_socket.connectToServer("ultimaker-cura")
  20. single_instance_socket.waitForConnected(msecs = 3000) # wait for 3 seconds
  21. if single_instance_socket.state() != QLocalSocket.ConnectedState:
  22. return False
  23. # We only send the files that need to be opened.
  24. if not self._files_to_open:
  25. Logger.log("i", "No file need to be opened, do nothing.")
  26. return True
  27. if single_instance_socket.state() == QLocalSocket.ConnectedState:
  28. Logger.log("i", "Connection has been made to the single-instance Cura socket.")
  29. # Protocol is one line of JSON terminated with a carriage return.
  30. # "command" field is required and holds the name of the command to execute.
  31. # Other fields depend on the command.
  32. payload = {"command": "clear-all"}
  33. single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
  34. payload = {"command": "focus"}
  35. single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
  36. for filename in self._files_to_open:
  37. payload = {"command": "open", "filePath": os.path.abspath(filename)}
  38. single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
  39. payload = {"command": "close-connection"}
  40. single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
  41. single_instance_socket.flush()
  42. single_instance_socket.waitForDisconnected()
  43. return True
  44. def startServer(self) -> None:
  45. self._single_instance_server = QLocalServer()
  46. self._single_instance_server.newConnection.connect(self._onClientConnected)
  47. self._single_instance_server.listen("ultimaker-cura")
  48. def _onClientConnected(self):
  49. Logger.log("i", "New connection recevied on our single-instance server")
  50. connection = self._single_instance_server.nextPendingConnection()
  51. if connection is not None:
  52. connection.readyRead.connect(lambda c = connection: self.__readCommands(c))
  53. def __readCommands(self, connection):
  54. line = connection.readLine()
  55. while len(line) != 0: # There is also a .canReadLine()
  56. try:
  57. payload = json.loads(str(line, encoding = "ascii").strip())
  58. command = payload["command"]
  59. # Command: Remove all models from the build plate.
  60. if command == "clear-all":
  61. self._application.callLater(lambda: self._application.deleteAll())
  62. # Command: Load a model file
  63. elif command == "open":
  64. self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
  65. # Command: Activate the window and bring it to the top.
  66. elif command == "focus":
  67. # Operating systems these days prevent windows from moving around by themselves.
  68. # 'alert' or flashing the icon in the taskbar is the best thing we do now.
  69. self._application.callLater(lambda: self._application.getMainWindow().alert(0))
  70. # Command: Close the socket connection. We're done.
  71. elif command == "close-connection":
  72. connection.close()
  73. else:
  74. Logger.log("w", "Received an unrecognized command " + str(command))
  75. except json.decoder.JSONDecodeError as ex:
  76. Logger.log("w", "Unable to parse JSON command '%s': %s", line, repr(ex))
  77. line = connection.readLine()