From 1d946085d375d28299297836673c06051daf6372 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 30 Jan 2018 13:40:47 +0100 Subject: [PATCH 01/13] Make crash dialog available before Application starts CURA-4895 --- cura/CrashHandler.py | 14 +++++++------- cura/CuraApplication.py | 11 +++++++---- cura_app.py | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 0c6740f740..8709461da3 100644 --- a/cura/CrashHandler.py +++ b/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 @@ -14,7 +13,7 @@ import ssl import urllib.request import urllib.error -from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication +from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox from UM.Application import Application @@ -49,10 +48,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 @@ -67,10 +67,6 @@ class CrashHandler: if not CuraDebugMode and exception_type not in fatal_exception_types: return - application = QCoreApplication.instance() - if not application: - sys.exit(1) - self.dialog = QDialog() self._createDialog() @@ -79,6 +75,7 @@ class CrashHandler: self.dialog.setMinimumWidth(640) self.dialog.setMinimumHeight(640) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) + self.dialog.finished.connect(self._close) layout = QVBoxLayout(self.dialog) @@ -89,6 +86,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", """

A fatal error has occurred. Please send us this Crash Report to fix the problem

diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8df9f01e91..301215005e 100755 --- a/cura/CuraApplication.py +++ b/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 @@ -256,7 +257,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 +410,8 @@ class CuraApplication(QtApplication): self.getCuraSceneController().setActiveBuildPlate(0) # Initialize + CuraApplication.Created = True + def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -503,7 +506,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 +735,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_() diff --git a/cura_app.py b/cura_app.py index b9afb9bbcc..624228b715 100755 --- a/cura_app.py +++ b/cura_app.py @@ -71,8 +71,40 @@ 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 not has_started: + CuraApplication.getInstance().removePostedEvents(None) + _crash_handler.show() + sys.exit(CuraApplication.getInstance().exec_()) + else: + _crash_handler.show() + sys.exit(1) + else: + application = QApplication(sys.argv) + _crash_handler = CrashHandler(hook_type, value, traceback, has_started) + _crash_handler.dialog.show() + sys.exit(application.exec_()) if not known_args["debug"]: sys.excepthook = exceptHook From 6bbc18d51e788dad525934963a8765f9758ff810 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 30 Jan 2018 15:52:20 +0100 Subject: [PATCH 02/13] Make an extra early crash dialog CURA-4895 An early crash dialog showing options to backup the current user data and reset the configuration files. --- cura/CrashHandler.py | 130 ++++++++++++++++++++++++++++++++++++++-- cura/CuraApplication.py | 1 - cura_app.py | 2 +- 3 files changed, 125 insertions(+), 8 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 8709461da3..58f172e5a9 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -12,9 +12,10 @@ import json import ssl import urllib.request import urllib.error +import shutil from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton from UM.Application import Application from UM.Logger import Logger @@ -67,15 +68,120 @@ class CrashHandler: if not CuraDebugMode and exception_type not in fatal_exception_types: return + 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", """

A fatal error has occurred.

+

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.

+

Please send us this Crash Report to fix the problem.

+ """)) + 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(180) + 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): + # TODO: 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) + + idx = 0 + file_name = base_name + "_" + str(time.time()) + zip_file_path = os.path.join(root_dir, file_name + ".zip") + while os.path.exists(zip_file_path): + idx += 1 + file_name = base_name + "_" + str(time.time()) + "_" + 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) + # 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")) - self.dialog.finished.connect(self._close) + # 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) @@ -249,9 +355,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 +379,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) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 301215005e..81956c04f1 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -134,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) diff --git a/cura_app.py b/cura_app.py index 624228b715..a4099881d1 100755 --- a/cura_app.py +++ b/cura_app.py @@ -103,7 +103,7 @@ def exceptHook(hook_type, value, traceback): else: application = QApplication(sys.argv) _crash_handler = CrashHandler(hook_type, value, traceback, has_started) - _crash_handler.dialog.show() + _crash_handler.early_crash_dialog.show() sys.exit(application.exec_()) if not known_args["debug"]: From aefe24222f05a183f88473648eca55f060bd54e3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 30 Jan 2018 16:04:11 +0100 Subject: [PATCH 03/13] Add a CLI flag to test early crash CURA-4895 --- cura/CuraApplication.py | 4 ++++ cura_app.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 81956c04f1..c39ce1d079 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -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"))) diff --git a/cura_app.py b/cura_app.py index a4099881d1..79ef170ac9 100755 --- a/cura_app.py +++ b/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"]: From 30b1e74881bcfe85ffe035b24157f42918904861 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 30 Jan 2018 16:41:33 +0100 Subject: [PATCH 04/13] Fixes for early crash dialog CURA-4895 --- cura/CrashHandler.py | 8 +++++++- cura_app.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 58f172e5a9..3ddc878b40 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -13,6 +13,7 @@ import ssl import urllib.request import urllib.error import shutil +import sys from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton @@ -140,6 +141,11 @@ class CrashHandler: if cache_path not in (config_path, data_path): folders_to_remove.append(cache_path) + # need to close the redirected stdout and stderr files, otherwise, on Windows, those opened files will prevent + # the directory removal calls below. + sys.stdout.close() + sys.stderr.close() + for folder in folders_to_remove: shutil.rmtree(folder, ignore_errors = True) for folder in folders_to_backup: @@ -159,7 +165,7 @@ class CrashHandler: 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) + 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) diff --git a/cura_app.py b/cura_app.py index 79ef170ac9..4bab1d5a7b 100755 --- a/cura_app.py +++ b/cura_app.py @@ -101,7 +101,7 @@ def exceptHook(hook_type, value, traceback): _crash_handler = CrashHandler(hook_type, value, traceback, has_started) if not has_started: CuraApplication.getInstance().removePostedEvents(None) - _crash_handler.show() + _crash_handler.early_crash_dialog.show() sys.exit(CuraApplication.getInstance().exec_()) else: _crash_handler.show() From c2aa5fc2a2756404f8ded96218aeeea566d8cdfb Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 1 Feb 2018 13:12:23 +0100 Subject: [PATCH 05/13] Only initialise AutoSave after Application finishes its start up CURA-4895 --- cura/CuraApplication.py | 8 ++++++++ plugins/AutoSave/AutoSave.py | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c39ce1d079..e2efaeb517 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -106,6 +106,14 @@ if not MYPY: CuraDebugMode = False +# +# A global signal which is triggered when CuraApplication has finished its start up. +# This is used to initialise some plugins such as AutoSave which should only be started after the application passed +# the start up successfully. +# +applicationStarted = pyqtSignal() + + class CuraApplication(QtApplication): # SettingVersion represents the set of settings available in the machine/extruder definitions. # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 331f328f2d..30c12b5424 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/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,8 +17,6 @@ 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) @@ -28,6 +27,21 @@ class AutoSave(Extension): 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. + from cura.CuraApplication import applicationStarted + applicationStarted.connect(self.initialize) + + def initialize(self): + from cura.CuraApplication import applicationStarted + applicationStarted.disconnect(self.initialize) + + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._onGlobalStackChanged() + + self._triggerTimer() + def _triggerTimer(self, *args): if not self._saving: self._change_timer.start() From 547baff239524301b134d1378db5bc125f55c67c Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 1 Feb 2018 13:17:37 +0100 Subject: [PATCH 06/13] Always show crash dialog CURA-4895 --- cura/CrashHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 3ddc878b40..f49983143f 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -66,7 +66,7 @@ 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 exception_type not in fatal_exception_types: return self._send_report_checkbox = None From b59eadce1c6b90984999aeae7c437e09607f33be Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 1 Feb 2018 16:39:56 +0100 Subject: [PATCH 07/13] Fix AutoSave crashing the early crash dialog CURA-4895 --- cura/CrashHandler.py | 5 ++++- cura/CuraApplication.py | 8 -------- cura_app.py | 1 + plugins/AutoSave/AutoSave.py | 17 +++++++++++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index f49983143f..e5975b9b2b 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -66,7 +66,10 @@ class CrashHandler: for part in line.rstrip("\n").split("\n"): Logger.log("c", part) - if 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 self._send_report_checkbox = None diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index e2efaeb517..c39ce1d079 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -106,14 +106,6 @@ if not MYPY: CuraDebugMode = False -# -# A global signal which is triggered when CuraApplication has finished its start up. -# This is used to initialise some plugins such as AutoSave which should only be started after the application passed -# the start up successfully. -# -applicationStarted = pyqtSignal() - - class CuraApplication(QtApplication): # SettingVersion represents the set of settings available in the machine/extruder definitions. # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible diff --git a/cura_app.py b/cura_app.py index 4bab1d5a7b..d1cf215fc5 100755 --- a/cura_app.py +++ b/cura_app.py @@ -108,6 +108,7 @@ def exceptHook(hook_type, value, traceback): sys.exit(1) else: application = QApplication(sys.argv) + application.removePostedEvents(None) _crash_handler = CrashHandler(hook_type, value, traceback, has_started) _crash_handler.early_crash_dialog.show() sys.exit(application.exec_()) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 30c12b5424..a549e6ff0a 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -30,12 +30,21 @@ class AutoSave(Extension): # 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. - from cura.CuraApplication import applicationStarted - applicationStarted.connect(self.initialize) + 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): - from cura.CuraApplication import applicationStarted - applicationStarted.disconnect(self.initialize) + # 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 Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() From e1bca1ca5d2c646b057cd2d1f24c2a5d94a43f33 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 5 Feb 2018 10:57:12 +0100 Subject: [PATCH 08/13] CURA-4895 Don't run autosave settings if application has not been created or if not started --- plugins/AutoSave/AutoSave.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index a549e6ff0a..50b66885d4 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -67,6 +67,15 @@ class AutoSave(Extension): self._global_stack.containersChanged.connect(self._triggerTimer) def _onTimeout(self): + # only initialise if the application is created and has started + from cura.CuraApplication import CuraApplication + if not CuraApplication.Created: + self._change_timer.start() + return + if not CuraApplication.getInstance().started: + self._change_timer.start() + return + self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") From 915bb2e450ad2a710c4cdb6815c663904dbb91f5 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 5 Feb 2018 11:16:30 +0100 Subject: [PATCH 09/13] CURA-4895 Connect timer signal just if CuraApplication has been previously created and started --- plugins/AutoSave/AutoSave.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 50b66885d4..5fdac502b5 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -23,7 +23,6 @@ class AutoSave(Extension): 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 @@ -46,6 +45,7 @@ class AutoSave(Extension): self._init_timer.start() return + self._change_timer.timeout.connect(self._onTimeout) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() @@ -67,15 +67,6 @@ class AutoSave(Extension): self._global_stack.containersChanged.connect(self._triggerTimer) def _onTimeout(self): - # only initialise if the application is created and has started - from cura.CuraApplication import CuraApplication - if not CuraApplication.Created: - self._change_timer.start() - return - if not CuraApplication.getInstance().started: - self._change_timer.start() - return - self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") From 383319d631c644d29c9b7a1e98ad1ed3b574f48c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 5 Feb 2018 11:17:17 +0100 Subject: [PATCH 10/13] CURA-4895 Correctly format the date for the backup file --- cura/CrashHandler.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index e5975b9b2b..e90619aee5 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -144,23 +144,20 @@ class CrashHandler: if cache_path not in (config_path, data_path): folders_to_remove.append(cache_path) - # need to close the redirected stdout and stderr files, otherwise, on Windows, those opened files will prevent - # the directory removal calls below. - sys.stdout.close() - sys.stderr.close() - 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 + "_" + str(time.time()) + 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 + "_" + str(time.time()) + "_" + idx + 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 From e64fb9c7622d149ad3fdcaf2fb55469fdf361bf1 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 5 Feb 2018 14:47:11 +0100 Subject: [PATCH 11/13] Splash closed on early crash --- cura_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura_app.py b/cura_app.py index d1cf215fc5..d0170e5972 100755 --- a/cura_app.py +++ b/cura_app.py @@ -32,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) @@ -110,6 +110,7 @@ def exceptHook(hook_type, value, traceback): application = QApplication(sys.argv) application.removePostedEvents(None) _crash_handler = CrashHandler(hook_type, value, traceback, has_started) + CuraApplication.getInstance().closeSplash() _crash_handler.early_crash_dialog.show() sys.exit(application.exec_()) From a85a720184ab3d7f8cd7097375d00ff73dd4a805 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 5 Feb 2018 17:17:46 +0100 Subject: [PATCH 12/13] CURA-4895 Close the splash screen when the early crash dialog appears. Increase the size of 'show detailed crash report button' --- cura/CrashHandler.py | 8 +++++--- cura_app.py | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index e90619aee5..a6b2373031 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -72,8 +72,10 @@ class CrashHandler: if has_started and exception_type not in fatal_exception_types: return - self._send_report_checkbox = None - self.early_crash_dialog = self._createEarlyCrashDialog() + if not has_started: + self._send_report_checkbox = None + self.early_crash_dialog = self._createEarlyCrashDialog() + self.dialog = QDialog() self._createDialog() @@ -99,7 +101,7 @@ class CrashHandler: self._send_report_checkbox.setChecked(True) show_details_button = QPushButton(catalog.i18nc("@action:button", "Show detailed crash report"), dialog) - show_details_button.setMaximumWidth(180) + show_details_button.setMaximumWidth(200) show_details_button.clicked.connect(self._showDetailedReport) layout.addWidget(self._send_report_checkbox) diff --git a/cura_app.py b/cura_app.py index d0170e5972..6c2d1c2937 100755 --- a/cura_app.py +++ b/cura_app.py @@ -99,18 +99,21 @@ def exceptHook(hook_type, value, traceback): 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() - sys.exit(1) else: application = QApplication(sys.argv) application.removePostedEvents(None) _crash_handler = CrashHandler(hook_type, value, traceback, has_started) - CuraApplication.getInstance().closeSplash() + # 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_()) From 4438e064160c67ce4696d005353ed7be330eb5f3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 5 Feb 2018 17:21:51 +0100 Subject: [PATCH 13/13] Remove TODO CURA-4895 --- cura/CrashHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index a6b2373031..5796375897 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -124,7 +124,7 @@ class CrashHandler: os._exit(1) def _backupAndStartClean(self): - # TODO: backup the current cura directories and create clean ones + # 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