cura_app.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2022 Ultimaker B.V.
  3. # Cura is released under the terms of the LGPLv3 or higher.
  4. # Remove the working directory from sys.path.
  5. # This fixes a security issue where Cura could import Python packages from the
  6. # current working directory, and therefore be made to execute locally installed
  7. # code (e.g. in the user's home directory where AppImages by default run from).
  8. # See issue CURA-7081.
  9. import sys
  10. if "" in sys.path:
  11. sys.path.remove("")
  12. import argparse
  13. import faulthandler
  14. import os
  15. if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
  16. os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
  17. try:
  18. # try converting to integer
  19. os.environ["QT_QUICK_FLICKABLE_WHEEL_DECELERATION"] = str(int(os.environ["QT_QUICK_FLICKABLE_WHEEL_DECELERATION"]))
  20. except ValueError:
  21. os.environ["QT_QUICK_FLICKABLE_WHEEL_DECELERATION"] = "5000"
  22. os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
  23. os.environ["QT_OPENGL_DLL"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
  24. from PyQt6.QtNetwork import QSslConfiguration, QSslSocket
  25. from UM.Platform import Platform
  26. from cura import ApplicationMetadata
  27. from cura.ApplicationMetadata import CuraAppName
  28. from cura.CrashHandler import CrashHandler
  29. try:
  30. import sentry_sdk
  31. with_sentry_sdk = True
  32. except ImportError:
  33. with_sentry_sdk = False
  34. parser = argparse.ArgumentParser(prog = "cura",
  35. add_help = False)
  36. parser.add_argument("--debug",
  37. action = "store_true",
  38. default = False,
  39. help = "Turn on the debug mode by setting this option."
  40. )
  41. known_args = vars(parser.parse_known_args()[0])
  42. if with_sentry_sdk:
  43. sentry_env = "unknown" # Start off with a "IDK"
  44. if hasattr(sys, "frozen"):
  45. sentry_env = "production" # A frozen build has the possibility to be a "real" distribution.
  46. if ApplicationMetadata.CuraVersion == "master":
  47. sentry_env = "development" # Master is always a development version.
  48. elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
  49. sentry_env = "beta"
  50. elif "alpha" in ApplicationMetadata.CuraVersion or "ALPHA" in ApplicationMetadata.CuraVersion:
  51. sentry_env = "alpha"
  52. try:
  53. if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
  54. sentry_env = "nightly"
  55. except IndexError:
  56. pass
  57. # Errors to be ignored by Sentry
  58. ignore_errors = [KeyboardInterrupt, MemoryError]
  59. try:
  60. sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564",
  61. before_send = CrashHandler.sentryBeforeSend,
  62. environment = sentry_env,
  63. release = "cura%s" % ApplicationMetadata.CuraVersion,
  64. default_integrations = False,
  65. max_breadcrumbs = 300,
  66. server_name = "cura",
  67. ignore_errors = ignore_errors)
  68. except Exception:
  69. with_sentry_sdk = False
  70. if not known_args["debug"]:
  71. def get_cura_dir_path():
  72. if Platform.isWindows():
  73. appdata_path = os.getenv("APPDATA")
  74. if not appdata_path: #Defensive against the environment variable missing (should never happen).
  75. appdata_path = "."
  76. return os.path.join(appdata_path, CuraAppName)
  77. elif Platform.isLinux():
  78. return os.path.expanduser("~/.local/share/" + CuraAppName)
  79. elif Platform.isOSX():
  80. return os.path.expanduser("~/Library/Logs/" + CuraAppName)
  81. # Do not redirect stdout and stderr to files if we are running CLI.
  82. if hasattr(sys, "frozen") and "cli" not in os.path.basename(sys.argv[0]).lower():
  83. dirpath = get_cura_dir_path()
  84. os.makedirs(dirpath, exist_ok = True)
  85. sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
  86. sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w", encoding = "utf-8")
  87. # WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
  88. if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
  89. # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
  90. # The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
  91. try:
  92. import ctypes
  93. from ctypes.util import find_library
  94. libGL = find_library("GL")
  95. ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
  96. except:
  97. # GLES-only systems (e.g. ARM Mali) do not have libGL, ignore error
  98. pass
  99. # When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs.
  100. if Platform.isWindows() and hasattr(sys, "frozen"):
  101. try:
  102. del os.environ["PYTHONPATH"]
  103. except KeyError:
  104. pass
  105. # GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194
  106. # With AppImage 2 on Linux, the current working directory will be somewhere in /tmp/<rand>/usr, which is owned
  107. # by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory,
  108. # otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we
  109. # switch to a directory where the user has the ownership.
  110. if Platform.isLinux() and hasattr(sys, "frozen"):
  111. os.chdir(os.path.expanduser("~"))
  112. # WORKAROUND: GITHUB-704 GITHUB-708
  113. # It looks like setuptools creates a .pth file in
  114. # the default /usr/lib which causes the default site-packages
  115. # to be inserted into sys.path before PYTHONPATH.
  116. # This can cause issues such as having libsip loaded from
  117. # the system instead of the one provided with Cura, which causes
  118. # incompatibility issues with libArcus
  119. if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is used
  120. PYTHONPATH = os.environ["PYTHONPATH"].split(os.pathsep) # Get the value, split it..
  121. PYTHONPATH.reverse() # and reverse it, because we always insert at 1
  122. for PATH in PYTHONPATH: # Now beginning with the last PATH
  123. PATH_real = os.path.realpath(PATH) # Making the the path "real"
  124. if PATH_real in sys.path: # This should always work, but keep it to be sure..
  125. sys.path.remove(PATH_real)
  126. sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
  127. def exceptHook(hook_type, value, traceback):
  128. from cura.CrashHandler import CrashHandler
  129. from cura.CuraApplication import CuraApplication
  130. has_started = False
  131. if CuraApplication.Created:
  132. has_started = CuraApplication.getInstance().started
  133. #
  134. # When the exception hook is triggered, the QApplication may not have been initialized yet. In this case, we don't
  135. # have an QApplication to handle the event loop, which is required by the Crash Dialog.
  136. # The flag "CuraApplication.Created" is set to True when CuraApplication finishes its constructor call.
  137. #
  138. # Before the "started" flag is set to True, the Qt event loop has not started yet. The event loop is a blocking
  139. # call to the QApplication.exec(). In this case, we need to:
  140. # 1. Remove all scheduled events so no more unnecessary events will be processed, such as loading the main dialog,
  141. # loading the machine, etc.
  142. # 2. Start the Qt event loop with exec() and show the Crash Dialog.
  143. #
  144. # If the application has finished its initialization and was running fine, and then something causes a crash,
  145. # we run the old routine to show the Crash Dialog.
  146. #
  147. from PyQt6.QtWidgets import QApplication
  148. if CuraApplication.Created:
  149. _crash_handler = CrashHandler(hook_type, value, traceback, has_started)
  150. if CuraApplication.splash is not None:
  151. CuraApplication.splash.close()
  152. if not has_started:
  153. CuraApplication.getInstance().removePostedEvents(None)
  154. _crash_handler.early_crash_dialog.show()
  155. sys.exit(CuraApplication.getInstance().exec())
  156. else:
  157. _crash_handler.show()
  158. else:
  159. application = QApplication(sys.argv)
  160. application.removePostedEvents(None)
  161. _crash_handler = CrashHandler(hook_type, value, traceback, has_started)
  162. # This means the QtApplication could be created and so the splash screen. Then Cura closes it
  163. if CuraApplication.splash is not None:
  164. CuraApplication.splash.close()
  165. _crash_handler.early_crash_dialog.show()
  166. sys.exit(application.exec())
  167. # Set exception hook to use the crash dialog handler
  168. sys.excepthook = exceptHook
  169. # Enable dumping traceback for all threads
  170. if sys.stderr and not sys.stderr.closed:
  171. faulthandler.enable(file = sys.stderr, all_threads = True)
  172. elif sys.stdout and not sys.stdout.closed:
  173. faulthandler.enable(file = sys.stdout, all_threads = True)
  174. from cura.CuraApplication import CuraApplication
  175. # WORKAROUND: CURA-6739
  176. # The CTM file loading module in Trimesh requires the OpenCTM library to be dynamically loaded. It uses
  177. # ctypes.util.find_library() to find libopenctm.dylib, but this doesn't seem to look in the ".app" application folder
  178. # on Mac OS X. Adding the search path to environment variables such as DYLD_LIBRARY_PATH and DYLD_FALLBACK_LIBRARY_PATH
  179. # makes it work. The workaround here uses DYLD_FALLBACK_LIBRARY_PATH.
  180. if Platform.isOSX() and getattr(sys, "frozen", False):
  181. old_env = os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", "")
  182. # This is where libopenctm.so is in the .app folder.
  183. search_path = os.path.join(CuraApplication.getInstallPrefix(), "MacOS")
  184. path_list = old_env.split(":")
  185. if search_path not in path_list:
  186. path_list.append(search_path)
  187. os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = ":".join(path_list)
  188. import trimesh.exchange.load
  189. os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = old_env
  190. # WORKAROUND: CURA-6739
  191. # Similar CTM file loading fix for Linux, but NOTE THAT this doesn't work directly with Python 3.5.7. There's a fix
  192. # for ctypes.util.find_library() in Python 3.6 and 3.7. That fix makes sure that find_library() will check
  193. # LD_LIBRARY_PATH. With Python 3.5, that fix needs to be backported to make this workaround work.
  194. if Platform.isLinux() and getattr(sys, "frozen", False):
  195. old_env = os.environ.get("LD_LIBRARY_PATH", "")
  196. # This is where libopenctm.so is in the AppImage.
  197. search_path = os.path.join(CuraApplication.getInstallPrefix(), "bin")
  198. path_list = old_env.split(":")
  199. if search_path not in path_list:
  200. path_list.append(search_path)
  201. os.environ["LD_LIBRARY_PATH"] = ":".join(path_list)
  202. import trimesh.exchange.load
  203. os.environ["LD_LIBRARY_PATH"] = old_env
  204. # WORKAROUND: Cura#5488
  205. # When using the KDE qqc2-desktop-style, the UI layout is completely broken, and
  206. # even worse, it crashes when switching to the "Preview" pane.
  207. if Platform.isLinux():
  208. os.environ["QT_QUICK_CONTROLS_STYLE"] = "default"
  209. if ApplicationMetadata.CuraDebugMode:
  210. ssl_conf = QSslConfiguration.defaultConfiguration()
  211. ssl_conf.setPeerVerifyMode(QSslSocket.PeerVerifyMode.VerifyNone)
  212. QSslConfiguration.setDefaultConfiguration(ssl_conf)
  213. app = CuraApplication()
  214. app.run()