Browse Source

Merge branch 'bak' into 3.2

Diego Prado Gesto 7 years ago
parent
commit
1e1f49ad53
4 changed files with 221 additions and 23 deletions
  1. 138 12
      cura/CrashHandler.py
  2. 11 5
      cura/CuraApplication.py
  3. 46 3
      cura_app.py
  4. 26 3
      plugins/AutoSave/AutoSave.py

+ 138 - 12
cura/CrashHandler.py

@@ -1,7 +1,6 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-import sys
 import platform
 import traceback
 import faulthandler
@@ -13,9 +12,11 @@ import json
 import ssl
 import urllib.request
 import urllib.error
+import shutil
+import sys
 
-from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox
+from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
 
 from UM.Application import Application
 from UM.Logger import Logger
@@ -49,10 +50,11 @@ fatal_exception_types = [
 class CrashHandler:
     crash_url = "https://stats.ultimaker.com/api/cura"
 
-    def __init__(self, exception_type, value, tb):
+    def __init__(self, exception_type, value, tb, has_started = True):
         self.exception_type = exception_type
         self.value = value
         self.traceback = tb
+        self.has_started = has_started
         self.dialog = None # Don't create a QDialog before there is a QApplication
 
         # While we create the GUI, the information will be stored for sending afterwards
@@ -64,21 +66,130 @@ class CrashHandler:
             for part in line.rstrip("\n").split("\n"):
                 Logger.log("c", part)
 
-        if not CuraDebugMode and exception_type not in fatal_exception_types:
+        # If Cura has fully started, we only show fatal errors.
+        # If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
+        # without any information.
+        if has_started and exception_type not in fatal_exception_types:
             return
 
-        application = QCoreApplication.instance()
-        if not application:
-            sys.exit(1)
+        if not has_started:
+            self._send_report_checkbox = None
+            self.early_crash_dialog = self._createEarlyCrashDialog()
 
         self.dialog = QDialog()
         self._createDialog()
 
+    def _createEarlyCrashDialog(self):
+        dialog = QDialog()
+        dialog.setMinimumWidth(500)
+        dialog.setMinimumHeight(170)
+        dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed"))
+        dialog.finished.connect(self._closeEarlyCrashDialog)
+
+        layout = QVBoxLayout(dialog)
+
+        label = QLabel()
+        label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred.</p></b>
+                    <p>Unfortunately, Cura encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.</p>
+                    <p>Please send us this Crash Report to fix the problem.</p>
+                """))
+        label.setWordWrap(True)
+        layout.addWidget(label)
+
+        # "send report" check box and show details
+        self._send_report_checkbox = QCheckBox(catalog.i18nc("@action:button", "Send crash report to Ultimaker"), dialog)
+        self._send_report_checkbox.setChecked(True)
+
+        show_details_button = QPushButton(catalog.i18nc("@action:button", "Show detailed crash report"), dialog)
+        show_details_button.setMaximumWidth(200)
+        show_details_button.clicked.connect(self._showDetailedReport)
+
+        layout.addWidget(self._send_report_checkbox)
+        layout.addWidget(show_details_button)
+
+        # "backup and start clean" and "close" buttons
+        buttons = QDialogButtonBox()
+        buttons.addButton(QDialogButtonBox.Close)
+        buttons.addButton(catalog.i18nc("@action:button", "Backup and Reset Configuration"), QDialogButtonBox.AcceptRole)
+        buttons.rejected.connect(self._closeEarlyCrashDialog)
+        buttons.accepted.connect(self._backupAndStartClean)
+
+        layout.addWidget(buttons)
+
+        return dialog
+
+    def _closeEarlyCrashDialog(self):
+        if self._send_report_checkbox.isChecked():
+            self._sendCrashReport()
+        os._exit(1)
+
+    def _backupAndStartClean(self):
+        # backup the current cura directories and create clean ones
+        from cura.CuraVersion import CuraVersion
+        from UM.Resources import Resources
+        # The early crash may happen before those information is set in Resources, so we need to set them here to
+        # make sure that Resources can find the correct place.
+        Resources.ApplicationIdentifier = "cura"
+        Resources.ApplicationVersion = CuraVersion
+        config_path = Resources.getConfigStoragePath()
+        data_path = Resources.getDataStoragePath()
+        cache_path = Resources.getCacheStoragePath()
+
+        folders_to_backup = []
+        folders_to_remove = []  # only cache folder needs to be removed
+
+        folders_to_backup.append(config_path)
+        if data_path != config_path:
+            folders_to_backup.append(data_path)
+
+        # Only remove the cache folder if it's not the same as data or config
+        if cache_path not in (config_path, data_path):
+            folders_to_remove.append(cache_path)
+
+        for folder in folders_to_remove:
+            shutil.rmtree(folder, ignore_errors = True)
+        for folder in folders_to_backup:
+            base_name = os.path.basename(folder)
+            root_dir = os.path.dirname(folder)
+
+            import datetime
+            date_now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+            idx = 0
+            file_name = base_name + "_" + date_now
+            zip_file_path = os.path.join(root_dir, file_name + ".zip")
+            while os.path.exists(zip_file_path):
+                idx += 1
+                file_name = base_name + "_" + date_now + "_" + idx
+                zip_file_path = os.path.join(root_dir, file_name + ".zip")
+            try:
+                # remove the .zip extension because make_archive() adds it
+                zip_file_path = zip_file_path[:-4]
+                shutil.make_archive(zip_file_path, "zip", root_dir = root_dir, base_dir = base_name)
+
+                # remove the folder only when the backup is successful
+                shutil.rmtree(folder, ignore_errors = True)
+                # create an empty folder so Resources will not try to copy the old ones
+                os.makedirs(folder, 0o0755, exist_ok=True)
+
+            except Exception as e:
+                Logger.logException("e", "Failed to backup [%s] to file [%s]", folder, zip_file_path)
+                if not self.has_started:
+                    print("Failed to backup [%s] to file [%s]: %s", folder, zip_file_path, e)
+
+        self.early_crash_dialog.close()
+
+    def _showDetailedReport(self):
+        self.dialog.exec_()
+
     ##  Creates a modal dialog.
     def _createDialog(self):
         self.dialog.setMinimumWidth(640)
         self.dialog.setMinimumHeight(640)
         self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
+        # if the application has not fully started, this will be a detailed report dialog which should not
+        # close the application when it's closed.
+        if self.has_started:
+            self.dialog.finished.connect(self._close)
 
         layout = QVBoxLayout(self.dialog)
 
@@ -89,6 +200,9 @@ class CrashHandler:
         layout.addWidget(self._userDescriptionWidget())
         layout.addWidget(self._buttonsWidget())
 
+    def _close(self):
+        os._exit(1)
+
     def _messageWidget(self):
         label = QLabel()
         label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
@@ -249,9 +363,13 @@ class CrashHandler:
     def _buttonsWidget(self):
         buttons = QDialogButtonBox()
         buttons.addButton(QDialogButtonBox.Close)
-        buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
+        # Like above, this will be served as a separate detailed report dialog if the application has not yet been
+        # fully loaded. In this case, "send report" will be a check box in the early crash dialog, so there is no
+        # need for this extra button.
+        if self.has_started:
+            buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
+            buttons.accepted.connect(self._sendCrashReport)
         buttons.rejected.connect(self.dialog.close)
-        buttons.accepted.connect(self._sendCrashReport)
 
         return buttons
 
@@ -269,15 +387,23 @@ class CrashHandler:
             kwoptions["context"] = ssl._create_unverified_context()
 
         Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
+        if not self.has_started:
+            print("Sending crash report info to [%s]...\n" % self.crash_url)
 
         try:
             f = urllib.request.urlopen(self.crash_url, **kwoptions)
             Logger.log("i", "Sent crash report info.")
+            if not self.has_started:
+                print("Sent crash report info.\n")
             f.close()
-        except urllib.error.HTTPError:
+        except urllib.error.HTTPError as e:
             Logger.logException("e", "An HTTP error occurred while trying to send crash report")
-        except Exception:  # We don't want any exception to cause problems
+            if not self.has_started:
+                print("An HTTP error occurred while trying to send crash report: %s" % e)
+        except Exception as e:  # We don't want any exception to cause problems
             Logger.logException("e", "An exception occurred while trying to send crash report")
+            if not self.has_started:
+                print("An exception occurred while trying to send crash report: %s" % e)
 
         os._exit(1)
 

+ 11 - 5
cura/CuraApplication.py

@@ -1,5 +1,4 @@
 # Copyright (c) 2017 Ultimaker B.V.
-# Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalSocket
@@ -113,6 +112,8 @@ class CuraApplication(QtApplication):
     # changes of the settings.
     SettingVersion = 4
 
+    Created = False
+
     class ResourceTypes:
         QmlFiles = Resources.UserType + 1
         Firmware = Resources.UserType + 2
@@ -133,7 +134,6 @@ class CuraApplication(QtApplication):
     stacksValidationFinished = pyqtSignal()  # Emitted whenever a validation is finished
 
     def __init__(self, **kwargs):
-
         # this list of dir names will be used by UM to detect an old cura directory
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
             Resources.addExpectedDirNameInData(dir_name)
@@ -223,6 +223,10 @@ class CuraApplication(QtApplication):
                          tray_icon_name = "cura-icon-32.png",
                          **kwargs)
 
+        # FOR TESTING ONLY
+        if kwargs["parsed_command_line"].get("trigger_early_crash", False):
+            1/0
+
         self.default_theme = "cura-light"
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -256,7 +260,7 @@ class CuraApplication(QtApplication):
         self._center_after_select = False
         self._camera_animation = None
         self._cura_actions = None
-        self._started = False
+        self.started = False
 
         self._message_box_callback = None
         self._message_box_callback_arguments = []
@@ -409,6 +413,8 @@ class CuraApplication(QtApplication):
 
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
 
+        CuraApplication.Created = True
+
     def _onEngineCreated(self):
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 
@@ -503,7 +509,7 @@ class CuraApplication(QtApplication):
     #
     #   Note that the AutoSave plugin also calls this method.
     def saveSettings(self):
-        if not self._started: # Do not do saving during application start
+        if not self.started: # Do not do saving during application start
             return
 
         ContainerRegistry.getInstance().saveDirtyContainers()
@@ -732,7 +738,7 @@ class CuraApplication(QtApplication):
             for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
                 self._openFile(file_name)
 
-            self._started = True
+            self.started = True
 
             self.exec_()
 

+ 46 - 3
cura_app.py

@@ -16,6 +16,12 @@ parser.add_argument('--debug',
                     default = False,
                     help = "Turn on the debug mode by setting this option."
                     )
+parser.add_argument('--trigger-early-crash',
+                    dest = 'trigger_early_crash',
+                    action = 'store_true',
+                    default = False,
+                    help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
+                    )
 known_args = vars(parser.parse_known_args()[0])
 
 if not known_args["debug"]:
@@ -26,7 +32,7 @@ if not known_args["debug"]:
             return os.path.expanduser("~/.local/share/cura")
         elif Platform.isOSX():
             return os.path.expanduser("~/Library/Logs/cura")
-    
+
     if hasattr(sys, "frozen"):
         dirpath = get_cura_dir_path()
         os.makedirs(dirpath, exist_ok = True)
@@ -71,8 +77,45 @@ if "PYTHONPATH" in os.environ.keys():                       # If PYTHONPATH is u
 
 def exceptHook(hook_type, value, traceback):
     from cura.CrashHandler import CrashHandler
-    _crash_handler = CrashHandler(hook_type, value, traceback)
-    _crash_handler.show()
+    from cura.CuraApplication import CuraApplication
+    has_started = False
+    if CuraApplication.Created:
+        has_started = CuraApplication.getInstance().started
+
+    #
+    # When the exception hook is triggered, the QApplication may not have been initialized yet. In this case, we don't
+    # have an QApplication to handle the event loop, which is required by the Crash Dialog.
+    # The flag "CuraApplication.Created" is set to True when CuraApplication finishes its constructor call.
+    #
+    # Before the "started" flag is set to True, the Qt event loop has not started yet. The event loop is a blocking
+    # call to the QApplication.exec_(). In this case, we need to:
+    #   1. Remove all scheduled events so no more unnecessary events will be processed, such as loading the main dialog,
+    #      loading the machine, etc.
+    #   2. Start the Qt event loop with exec_() and show the Crash Dialog.
+    #
+    # If the application has finished its initialization and was running fine, and then something causes a crash,
+    # we run the old routine to show the Crash Dialog.
+    #
+    from PyQt5.Qt import QApplication
+    if CuraApplication.Created:
+        _crash_handler = CrashHandler(hook_type, value, traceback, has_started)
+        if CuraApplication.splash is not None:
+            CuraApplication.splash.close()
+        if not has_started:
+            CuraApplication.getInstance().removePostedEvents(None)
+            _crash_handler.early_crash_dialog.show()
+            sys.exit(CuraApplication.getInstance().exec_())
+        else:
+            _crash_handler.show()
+    else:
+        application = QApplication(sys.argv)
+        application.removePostedEvents(None)
+        _crash_handler = CrashHandler(hook_type, value, traceback, has_started)
+        # This means the QtApplication could be created and so the splash screen. Then Cura closes it
+        if CuraApplication.splash is not None:
+            CuraApplication.splash.close()
+        _crash_handler.early_crash_dialog.show()
+        sys.exit(application.exec_())
 
 if not known_args["debug"]:
     sys.excepthook = exceptHook

+ 26 - 3
plugins/AutoSave/AutoSave.py

@@ -9,6 +9,7 @@ from UM.Application import Application
 from UM.Resources import Resources
 from UM.Logger import Logger
 
+
 class AutoSave(Extension):
     def __init__(self):
         super().__init__()
@@ -16,18 +17,40 @@ class AutoSave(Extension):
         Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
 
         self._global_stack = None
-        Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
-        self._onGlobalStackChanged()
 
         Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
 
         self._change_timer = QTimer()
         self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
         self._change_timer.setSingleShot(True)
-        self._change_timer.timeout.connect(self._onTimeout)
 
         self._saving = False
 
+        # At this point, the Application instance has not finished its constructor call yet, so directly using something
+        # like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
+        # application finishes its start up successfully.
+        self._init_timer = QTimer()
+        self._init_timer.setInterval(1000)
+        self._init_timer.setSingleShot(True)
+        self._init_timer.timeout.connect(self.initialize)
+        self._init_timer.start()
+
+    def initialize(self):
+        # only initialise if the application is created and has started
+        from cura.CuraApplication import CuraApplication
+        if not CuraApplication.Created:
+            self._init_timer.start()
+            return
+        if not CuraApplication.getInstance().started:
+            self._init_timer.start()
+            return
+
+        self._change_timer.timeout.connect(self._onTimeout)
+        Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
+        self._onGlobalStackChanged()
+
+        self._triggerTimer()
+
     def _triggerTimer(self, *args):
         if not self._saving:
             self._change_timer.start()