diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py
index 7008ba64d2..04f04a1c37 100644
--- a/cura/CrashHandler.py
+++ b/cura/CrashHandler.py
@@ -1,18 +1,27 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
import sys
import platform
import traceback
-import webbrowser
import faulthandler
import tempfile
import os
-import urllib
+import os.path
+import time
+import json
+import ssl
+import urllib.request
+import urllib.error
-from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QCoreApplication
-from PyQt5.QtGui import QPixmap
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QVBoxLayout, QLabel, QTextEdit
+from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QPushButton
from UM.Logger import Logger
+from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
+from UM.Platform import Platform
+
catalog = i18nCatalog("cura")
MYPY = False
@@ -35,83 +44,238 @@ fatal_exception_types = [
SystemError,
]
-def show(exception_type, value, tb):
- Logger.log("c", "An uncaught exception has occurred!")
- for line in traceback.format_exception(exception_type, value, tb):
- for part in line.rstrip("\n").split("\n"):
- Logger.log("c", part)
+class CrashHandler:
+ crash_url = "https://stats.ultimaker.com/api/cura"
- if not CuraDebugMode and exception_type not in fatal_exception_types:
- return
+ def __init__(self, exception_type, value, tb):
- application = QCoreApplication.instance()
- if not application:
- sys.exit(1)
+ self.exception_type = exception_type
+ self.value = value
+ self.traceback = tb
- dialog = QDialog()
- dialog.setMinimumWidth(640)
- dialog.setMinimumHeight(640)
- dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
+ # While we create the GUI, the information will be stored for sending afterwards
+ self.data = dict()
+ self.data["time_stamp"] = time.time()
- layout = QVBoxLayout(dialog)
+ Logger.log("c", "An uncaught exception has occurred!")
+ for line in traceback.format_exception(exception_type, value, tb):
+ for part in line.rstrip("\n").split("\n"):
+ Logger.log("c", part)
- #label = QLabel(dialog)
- #pixmap = QPixmap()
- #try:
- # data = urllib.request.urlopen("http://www.randomkittengenerator.com/cats/rotator.php").read()
- # pixmap.loadFromData(data)
- #except:
- # try:
- # from UM.Resources import Resources
- # path = Resources.getPath(Resources.Images, "kitten.jpg")
- # pixmap.load(path)
- # except:
- # pass
- #pixmap = pixmap.scaled(150, 150)
- #label.setPixmap(pixmap)
- #label.setAlignment(Qt.AlignCenter)
- #layout.addWidget(label)
+ if not CuraDebugMode and exception_type not in fatal_exception_types:
+ return
- label = QLabel(dialog)
- layout.addWidget(label)
+ application = QCoreApplication.instance()
+ if not application:
+ sys.exit(1)
- #label.setScaledContents(True)
- label.setText(catalog.i18nc("@label", """
A fatal exception has occurred that we could not recover from!
- Please use the information below to post a bug report at http://github.com/Ultimaker/Cura/issues
- """))
+ self._createDialog()
- textarea = QTextEdit(dialog)
- layout.addWidget(textarea)
+ ## Creates a modal dialog.
+ def _createDialog(self):
- try:
- from UM.Application import Application
- version = Application.getInstance().getVersion()
- except:
- version = "Unknown"
+ self.dialog = QDialog()
+ self.dialog.setMinimumWidth(640)
+ self.dialog.setMinimumHeight(640)
+ self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
- trace = "".join(traceback.format_exception(exception_type, value, tb))
+ layout = QVBoxLayout(self.dialog)
- crash_info = "Version: {0}\nPlatform: {1}\nQt: {2}\nPyQt: {3}\n\nException:\n{4}"
- crash_info = crash_info.format(version, platform.platform(), QT_VERSION_STR, PYQT_VERSION_STR, trace)
+ layout.addWidget(self._messageWidget())
+ layout.addWidget(self._informationWidget())
+ layout.addWidget(self._exceptionInfoWidget())
+ layout.addWidget(self._logInfoWidget())
+ layout.addWidget(self._userDescriptionWidget())
+ layout.addWidget(self._buttonsWidget())
- tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
- os.close(tmp_file_fd)
- with open(tmp_file_path, "w") as f:
- faulthandler.dump_traceback(f, all_threads=True)
- with open(tmp_file_path, "r") as f:
- data = f.read()
+ def _messageWidget(self):
+ label = QLabel()
+ label.setText(catalog.i18nc("@label crash message", """A fatal exception has occurred. Please send us this Crash Report to fix the problem
+ Please use the "Send report" button to post a bug report automatically to our servers
+ """))
- msg = "-------------------------\n"
- msg += data
- crash_info += "\n\n" + msg
+ return label
- textarea.setText(crash_info)
+ def _informationWidget(self):
+ group = QGroupBox()
+ group.setTitle(catalog.i18nc("@title:groupbox", "System information"))
+ layout = QVBoxLayout()
+ label = QLabel()
- buttons = QDialogButtonBox(QDialogButtonBox.Close, dialog)
- layout.addWidget(buttons)
- buttons.addButton(catalog.i18nc("@action:button", "Open Web Page"), QDialogButtonBox.HelpRole)
- buttons.rejected.connect(dialog.close)
- buttons.helpRequested.connect(lambda: webbrowser.open("http://github.com/Ultimaker/Cura/issues"))
+ try:
+ from UM.Application import Application
+ self.cura_version = Application.getInstance().getVersion()
+ except:
+ self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
- dialog.exec_()
- sys.exit(1)
+ crash_info = catalog.i18nc("@label Cura version", "Cura version: {version}
").format(version = self.cura_version)
+ crash_info += catalog.i18nc("@label Platform", "Platform: {platform}
").format(platform = platform.platform())
+ crash_info += catalog.i18nc("@label Qt version", "Qt version: {qt}
").format(qt = QT_VERSION_STR)
+ crash_info += catalog.i18nc("@label PyQt version", "PyQt version: {pyqt}
").format(pyqt = PYQT_VERSION_STR)
+ crash_info += catalog.i18nc("@label OpenGL", "OpenGL: {opengl}
").format(opengl = self._getOpenGLInfo())
+ label.setText(crash_info)
+
+ layout.addWidget(label)
+ group.setLayout(layout)
+
+ self.data["cura_version"] = self.cura_version
+ self.data["os"] = {"type": platform.system(), "version": platform.version()}
+ self.data["qt_version"] = QT_VERSION_STR
+ self.data["pyqt_version"] = PYQT_VERSION_STR
+
+ return group
+
+ def _getOpenGLInfo(self):
+ info = ""
+ info += catalog.i18nc("@label OpenGL version", "- OpenGL Version: {version}
").format(version = OpenGL.getInstance().getOpenGLVersion())
+ info += catalog.i18nc("@label OpenGL vendor", "- OpenGL Vendor: {vendor}
").format(vendor = OpenGL.getInstance().getGPUVendorName())
+ info += catalog.i18nc("@label OpenGL renderer", "- OpenGL Renderer: {renderer}
").format(renderer = OpenGL.getInstance().getGPUType())
+ info += "
"
+
+ self.data["opengl"] = {"version": OpenGL.getInstance().getOpenGLVersion(), "vendor": OpenGL.getInstance().getGPUVendorName(), "type": OpenGL.getInstance().getGPUType()}
+
+ return info
+
+ def _exceptionInfoWidget(self):
+ group = QGroupBox()
+ group.setTitle(catalog.i18nc("@title:groupbox", "Exception traceback"))
+ layout = QVBoxLayout()
+
+ text_area = QTextEdit()
+ trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback)
+ trace = "".join(trace_dict)
+ text_area.setText(trace)
+ text_area.setReadOnly(True)
+
+ layout.addWidget(text_area)
+ group.setLayout(layout)
+
+ # Parsing all the information to fill the dictionary
+ summary = trace_dict[len(trace_dict)-1].rstrip("\n")
+ module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n")
+ module_split = module[0].split(", ")
+ filepath = module_split[0].split("\"")[1]
+ directory, filename = os.path.split(filepath)
+ line = int(module_split[1].lstrip("line "))
+ function = module_split[2].lstrip("in ")
+ code = module[1].lstrip(" ")
+
+ # Using this workaround for a cross-platform path splitting
+ split_path = []
+ folder_name = ""
+ # Split until reach folder "cura"
+ while folder_name != "cura":
+ directory, folder_name = os.path.split(directory)
+ if not folder_name:
+ break
+ split_path.append(folder_name)
+
+ # Look for plugins. If it's not a plugin, the current cura version is set
+ isPlugin = False
+ module_version = self.cura_version
+ module_name = "Cura"
+ if split_path.__contains__("plugins"):
+ isPlugin = True
+ # Look backwards until plugin.json is found
+ directory, name = os.path.split(filepath)
+ while not os.listdir(directory).__contains__("plugin.json"):
+ directory, name = os.path.split(directory)
+
+ json_metadata_file = os.path.join(directory, "plugin.json")
+ try:
+ with open(json_metadata_file, "r") as f:
+ try:
+ metadata = json.loads(f.read())
+ module_version = metadata["version"]
+ module_name = metadata["name"]
+ except json.decoder.JSONDecodeError:
+ # Not throw new exceptions
+ Logger.logException("e", "Failed to parse plugin.json for plugin %s", name)
+ except:
+ # Not throw new exceptions
+ pass
+
+ exception_dict = dict()
+ exception_dict["traceback"] = {"summary": summary, "full_trace": trace}
+ exception_dict["location"] = {"path": filepath, "file": filename, "function": function, "code": code, "line": line,
+ "module_name": module_name, "version": module_version, "is_plugin": isPlugin}
+ self.data["exception"] = exception_dict
+
+ return group
+
+ def _logInfoWidget(self):
+ group = QGroupBox()
+ group.setTitle(catalog.i18nc("@title:groupbox", "Logs"))
+ layout = QVBoxLayout()
+
+ text_area = QTextEdit()
+ tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
+ os.close(tmp_file_fd)
+ with open(tmp_file_path, "w") as f:
+ faulthandler.dump_traceback(f, all_threads=True)
+ with open(tmp_file_path, "r") as f:
+ logdata = f.read()
+
+ text_area.setText(logdata)
+ text_area.setReadOnly(True)
+
+ layout.addWidget(text_area)
+ group.setLayout(layout)
+
+ self.data["log"] = logdata
+
+ return group
+
+
+ def _userDescriptionWidget(self):
+ group = QGroupBox()
+ group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
+ layout = QVBoxLayout()
+
+ # When sending the report, the user comments will be collected
+ self.user_description_text_area = QTextEdit()
+ self.user_description_text_area.setFocus(True)
+
+ layout.addWidget(self.user_description_text_area)
+ group.setLayout(layout)
+
+ return group
+
+ def _buttonsWidget(self):
+ buttons = QDialogButtonBox()
+ buttons.addButton(QDialogButtonBox.Close)
+ buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
+ buttons.rejected.connect(self.dialog.close)
+ buttons.accepted.connect(self._sendCrashReport)
+
+ return buttons
+
+ def _sendCrashReport(self):
+ # Before sending data, the user comments are stored
+ self.data["user_info"] = self.user_description_text_area.toPlainText()
+
+ # Convert data to bytes
+ binary_data = json.dumps(self.data).encode("utf-8")
+
+ # Submit data
+ kwoptions = {"data": binary_data, "timeout": 5}
+
+ if Platform.isOSX():
+ kwoptions["context"] = ssl._create_unverified_context()
+
+ Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
+
+ try:
+ f = urllib.request.urlopen(self.crash_url, **kwoptions)
+ Logger.log("i", "Sent crash report info.")
+ f.close()
+ except urllib.error.HTTPError:
+ Logger.logException("e", "An HTTP error occurred while trying to send crash report")
+ except Exception: # We don't want any exception to cause problems
+ Logger.logException("e", "An exception occurred while trying to send crash report")
+
+ os._exit(1)
+
+ def show(self):
+ self.dialog.exec_()
+ os._exit(1)
\ No newline at end of file
diff --git a/cura_app.py b/cura_app.py
index 6869fd2111..d725bc1200 100755
--- a/cura_app.py
+++ b/cura_app.py
@@ -41,8 +41,9 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
def exceptHook(hook_type, value, traceback):
- import cura.CrashHandler
- cura.CrashHandler.show(hook_type, value, traceback)
+ from cura.CrashHandler import CrashHandler
+ _crash_handler = CrashHandler(hook_type, value, traceback)
+ _crash_handler.show()
sys.excepthook = exceptHook