Merge branch 'bak' into 3.2

This commit is contained in:
Diego Prado Gesto 2018-02-05 17:27:11 +01:00
commit 1e1f49ad53
4 changed files with 221 additions and 23 deletions

View File

@ -1,7 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys
import platform import platform
import traceback import traceback
import faulthandler import faulthandler
@ -13,9 +12,11 @@ import json
import ssl import ssl
import urllib.request import urllib.request
import urllib.error import urllib.error
import shutil
import sys
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 PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -49,10 +50,11 @@ fatal_exception_types = [
class CrashHandler: class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura" 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.exception_type = exception_type
self.value = value self.value = value
self.traceback = tb self.traceback = tb
self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication 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 # 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"): for part in line.rstrip("\n").split("\n"):
Logger.log("c", part) 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 return
application = QCoreApplication.instance() if not has_started:
if not application: self._send_report_checkbox = None
sys.exit(1) self.early_crash_dialog = self._createEarlyCrashDialog()
self.dialog = QDialog() self.dialog = QDialog()
self._createDialog() 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. ## Creates a modal dialog.
def _createDialog(self): def _createDialog(self):
self.dialog.setMinimumWidth(640) self.dialog.setMinimumWidth(640)
self.dialog.setMinimumHeight(640) self.dialog.setMinimumHeight(640)
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) 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) layout = QVBoxLayout(self.dialog)
@ -89,6 +200,9 @@ class CrashHandler:
layout.addWidget(self._userDescriptionWidget()) layout.addWidget(self._userDescriptionWidget())
layout.addWidget(self._buttonsWidget()) layout.addWidget(self._buttonsWidget())
def _close(self):
os._exit(1)
def _messageWidget(self): def _messageWidget(self):
label = QLabel() 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> 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): def _buttonsWidget(self):
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close) 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.rejected.connect(self.dialog.close)
buttons.accepted.connect(self._sendCrashReport)
return buttons return buttons
@ -269,15 +387,23 @@ class CrashHandler:
kwoptions["context"] = ssl._create_unverified_context() kwoptions["context"] = ssl._create_unverified_context()
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) 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: try:
f = urllib.request.urlopen(self.crash_url, **kwoptions) f = urllib.request.urlopen(self.crash_url, **kwoptions)
Logger.log("i", "Sent crash report info.") Logger.log("i", "Sent crash report info.")
if not self.has_started:
print("Sent crash report info.\n")
f.close() 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") 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") 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) os._exit(1)

View File

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

View File

@ -16,6 +16,12 @@ parser.add_argument('--debug',
default = False, default = False,
help = "Turn on the debug mode by setting this option." 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]) known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]: if not known_args["debug"]:
@ -26,7 +32,7 @@ if not known_args["debug"]:
return os.path.expanduser("~/.local/share/cura") return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX(): elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura") return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"): if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path() dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True) 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): def exceptHook(hook_type, value, traceback):
from cura.CrashHandler import CrashHandler from cura.CrashHandler import CrashHandler
_crash_handler = CrashHandler(hook_type, value, traceback) from cura.CuraApplication import CuraApplication
_crash_handler.show() 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"]: if not known_args["debug"]:
sys.excepthook = exceptHook sys.excepthook = exceptHook

View File

@ -9,6 +9,7 @@ from UM.Application import Application
from UM.Resources import Resources from UM.Resources import Resources
from UM.Logger import Logger from UM.Logger import Logger
class AutoSave(Extension): class AutoSave(Extension):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -16,18 +17,40 @@ class AutoSave(Extension):
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer() self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True) self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False 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): def _triggerTimer(self, *args):
if not self._saving: if not self._saving:
self._change_timer.start() self._change_timer.start()