CrashHandler.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import sys
  4. import platform
  5. import traceback
  6. import faulthandler
  7. import tempfile
  8. import os
  9. import os.path
  10. import time
  11. import json
  12. import ssl
  13. import urllib.request
  14. import urllib.error
  15. from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
  16. from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox
  17. from UM.Application import Application
  18. from UM.Logger import Logger
  19. from UM.View.GL.OpenGL import OpenGL
  20. from UM.i18n import i18nCatalog
  21. from UM.Platform import Platform
  22. catalog = i18nCatalog("cura")
  23. MYPY = False
  24. if MYPY:
  25. CuraDebugMode = False
  26. else:
  27. try:
  28. from cura.CuraVersion import CuraDebugMode
  29. except ImportError:
  30. CuraDebugMode = False # [CodeStyle: Reflecting imported value]
  31. # List of exceptions that should be considered "fatal" and abort the program.
  32. # These are primarily some exception types that we simply cannot really recover from
  33. # (MemoryError and SystemError) and exceptions that indicate grave errors in the
  34. # code that cause the Python interpreter to fail (SyntaxError, ImportError).
  35. fatal_exception_types = [
  36. MemoryError,
  37. SyntaxError,
  38. ImportError,
  39. SystemError,
  40. ]
  41. class CrashHandler:
  42. crash_url = "https://stats.ultimaker.com/api/cura"
  43. def __init__(self, exception_type, value, tb):
  44. self.exception_type = exception_type
  45. self.value = value
  46. self.traceback = tb
  47. self.dialog = QDialog()
  48. # While we create the GUI, the information will be stored for sending afterwards
  49. self.data = dict()
  50. self.data["time_stamp"] = time.time()
  51. Logger.log("c", "An uncaught exception has occurred!")
  52. for line in traceback.format_exception(exception_type, value, tb):
  53. for part in line.rstrip("\n").split("\n"):
  54. Logger.log("c", part)
  55. if not CuraDebugMode and exception_type not in fatal_exception_types:
  56. return
  57. application = QCoreApplication.instance()
  58. if not application:
  59. sys.exit(1)
  60. self._createDialog()
  61. ## Creates a modal dialog.
  62. def _createDialog(self):
  63. self.dialog.setMinimumWidth(640)
  64. self.dialog.setMinimumHeight(640)
  65. self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
  66. layout = QVBoxLayout(self.dialog)
  67. layout.addWidget(self._messageWidget())
  68. layout.addWidget(self._informationWidget())
  69. layout.addWidget(self._exceptionInfoWidget())
  70. layout.addWidget(self._logInfoWidget())
  71. layout.addWidget(self._userDescriptionWidget())
  72. layout.addWidget(self._buttonsWidget())
  73. def _messageWidget(self):
  74. label = QLabel()
  75. label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal exception has occurred. Please send us this Crash Report to fix the problem</p></b>
  76. <p>Please use the "Send report" button to post a bug report automatically to our servers</p>
  77. """))
  78. return label
  79. def _informationWidget(self):
  80. group = QGroupBox()
  81. group.setTitle(catalog.i18nc("@title:groupbox", "System information"))
  82. layout = QVBoxLayout()
  83. label = QLabel()
  84. try:
  85. from UM.Application import Application
  86. self.cura_version = Application.getInstance().getVersion()
  87. except:
  88. self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
  89. crash_info = catalog.i18nc("@label Cura version", "<b>Cura version:</b> {version}<br/>").format(version = self.cura_version)
  90. crash_info += catalog.i18nc("@label Platform", "<b>Platform:</b> {platform}<br/>").format(platform = platform.platform())
  91. crash_info += catalog.i18nc("@label Qt version", "<b>Qt version:</b> {qt}<br/>").format(qt = QT_VERSION_STR)
  92. crash_info += catalog.i18nc("@label PyQt version", "<b>PyQt version:</b> {pyqt}<br/>").format(pyqt = PYQT_VERSION_STR)
  93. crash_info += catalog.i18nc("@label OpenGL", "<b>OpenGL:</b> {opengl}<br/>").format(opengl = self._getOpenGLInfo())
  94. label.setText(crash_info)
  95. layout.addWidget(label)
  96. group.setLayout(layout)
  97. self.data["cura_version"] = self.cura_version
  98. self.data["os"] = {"type": platform.system(), "version": platform.version()}
  99. self.data["qt_version"] = QT_VERSION_STR
  100. self.data["pyqt_version"] = PYQT_VERSION_STR
  101. return group
  102. def _getOpenGLInfo(self):
  103. info = "<ul>"
  104. info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = OpenGL.getInstance().getOpenGLVersion())
  105. info += catalog.i18nc("@label OpenGL vendor", "<li>OpenGL Vendor: {vendor}</li>").format(vendor = OpenGL.getInstance().getGPUVendorName())
  106. info += catalog.i18nc("@label OpenGL renderer", "<li>OpenGL Renderer: {renderer}</li>").format(renderer = OpenGL.getInstance().getGPUType())
  107. info += "</ul>"
  108. self.data["opengl"] = {"version": OpenGL.getInstance().getOpenGLVersion(), "vendor": OpenGL.getInstance().getGPUVendorName(), "type": OpenGL.getInstance().getGPUType()}
  109. return info
  110. def _exceptionInfoWidget(self):
  111. group = QGroupBox()
  112. group.setTitle(catalog.i18nc("@title:groupbox", "Exception traceback"))
  113. layout = QVBoxLayout()
  114. text_area = QTextEdit()
  115. trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback)
  116. trace = "".join(trace_dict)
  117. text_area.setText(trace)
  118. text_area.setReadOnly(True)
  119. layout.addWidget(text_area)
  120. group.setLayout(layout)
  121. # Parsing all the information to fill the dictionary
  122. summary = trace_dict[len(trace_dict)-1].rstrip("\n")
  123. module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n")
  124. module_split = module[0].split(", ")
  125. filepath = module_split[0].split("\"")[1]
  126. directory, filename = os.path.split(filepath)
  127. line = int(module_split[1].lstrip("line "))
  128. function = module_split[2].lstrip("in ")
  129. code = module[1].lstrip(" ")
  130. # Using this workaround for a cross-platform path splitting
  131. split_path = []
  132. folder_name = ""
  133. # Split until reach folder "cura"
  134. while folder_name != "cura":
  135. directory, folder_name = os.path.split(directory)
  136. if not folder_name:
  137. break
  138. split_path.append(folder_name)
  139. # Look for plugins. If it's not a plugin, the current cura version is set
  140. isPlugin = False
  141. module_version = self.cura_version
  142. module_name = "Cura"
  143. if split_path.__contains__("plugins"):
  144. isPlugin = True
  145. # Look backwards until plugin.json is found
  146. directory, name = os.path.split(filepath)
  147. while not os.listdir(directory).__contains__("plugin.json"):
  148. directory, name = os.path.split(directory)
  149. json_metadata_file = os.path.join(directory, "plugin.json")
  150. try:
  151. with open(json_metadata_file, "r") as f:
  152. try:
  153. metadata = json.loads(f.read())
  154. module_version = metadata["version"]
  155. module_name = metadata["name"]
  156. except json.decoder.JSONDecodeError:
  157. # Not throw new exceptions
  158. Logger.logException("e", "Failed to parse plugin.json for plugin %s", name)
  159. except:
  160. # Not throw new exceptions
  161. pass
  162. exception_dict = dict()
  163. exception_dict["traceback"] = {"summary": summary, "full_trace": trace}
  164. exception_dict["location"] = {"path": filepath, "file": filename, "function": function, "code": code, "line": line,
  165. "module_name": module_name, "version": module_version, "is_plugin": isPlugin}
  166. self.data["exception"] = exception_dict
  167. return group
  168. def _logInfoWidget(self):
  169. group = QGroupBox()
  170. group.setTitle(catalog.i18nc("@title:groupbox", "Logs"))
  171. layout = QVBoxLayout()
  172. text_area = QTextEdit()
  173. tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
  174. os.close(tmp_file_fd)
  175. with open(tmp_file_path, "w") as f:
  176. faulthandler.dump_traceback(f, all_threads=True)
  177. with open(tmp_file_path, "r") as f:
  178. logdata = f.read()
  179. text_area.setText(logdata)
  180. text_area.setReadOnly(True)
  181. layout.addWidget(text_area)
  182. group.setLayout(layout)
  183. self.data["log"] = logdata
  184. return group
  185. def _userDescriptionWidget(self):
  186. group = QGroupBox()
  187. group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
  188. layout = QVBoxLayout()
  189. # When sending the report, the user comments will be collected
  190. self.user_description_text_area = QTextEdit()
  191. self.user_description_text_area.setFocus(True)
  192. layout.addWidget(self.user_description_text_area)
  193. group.setLayout(layout)
  194. return group
  195. def _buttonsWidget(self):
  196. buttons = QDialogButtonBox()
  197. buttons.addButton(QDialogButtonBox.Close)
  198. buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
  199. buttons.rejected.connect(self.dialog.close)
  200. buttons.accepted.connect(self._sendCrashReport)
  201. return buttons
  202. def _sendCrashReport(self):
  203. # Before sending data, the user comments are stored
  204. self.data["user_info"] = self.user_description_text_area.toPlainText()
  205. # Convert data to bytes
  206. binary_data = json.dumps(self.data).encode("utf-8")
  207. # Submit data
  208. kwoptions = {"data": binary_data, "timeout": 5}
  209. if Platform.isOSX():
  210. kwoptions["context"] = ssl._create_unverified_context()
  211. Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
  212. try:
  213. f = urllib.request.urlopen(self.crash_url, **kwoptions)
  214. Logger.log("i", "Sent crash report info.")
  215. f.close()
  216. except urllib.error.HTTPError:
  217. Logger.logException("e", "An HTTP error occurred while trying to send crash report")
  218. except Exception: # We don't want any exception to cause problems
  219. Logger.logException("e", "An exception occurred while trying to send crash report")
  220. os._exit(1)
  221. def show(self):
  222. # must run the GUI code on the Qt thread, otherwise the widgets on the dialog won't react correctly.
  223. Application.getInstance().callLater(self._show)
  224. def _show(self):
  225. self.dialog.exec_()
  226. os._exit(1)