diff --git a/nw/__init__.py b/nw/__init__.py index 5ac300e4..c6f815e7 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -280,45 +280,10 @@ def main(sysArgs=None): nwApp.setWindowIcon(QIcon(CONFIG.appIcon)) nwApp.setOrganizationDomain(__domain__) - # We try to catch critical errors while setting up the main GUI - # by wrapping the main GUI in a try/except structure. This will - # not catch all exceptions for other parts of the application. - # For all other unhandled exceptions, we use a custom exception - # handler that pops a dialog box with the error message. + # Connect the exception handler before making the main GUI sys.excepthook = exceptionHandler - try: - nwGUI = GuiMain() - sys.exit(nwApp.exec_()) - - except Exception: - - from traceback import print_tb - from nw.error import formatHtmlErrMsg - - exType, exValue, exTrace = sys.exc_info() - - logger.critical("%s: %s" % (exType.__name__, str(exValue))) - print_tb(exTrace) - - try: - del nwApp - - errApp = QApplication([]) - errMsg = QErrorMessage() - errMsg.setWindowTitle("Critical Error") - errMsg.resize(800, 400) - errMsg.showMessage(( - "

A critical error has been encountered

" - "%s" - "

Shutting down ...

" - ) % formatHtmlErrMsg(exType, exValue, exTrace)) - errApp.exec_() - - except Exception as e: - logger.critical("Could not create error message dialog.") - logger.critical(str(e)) - - sys.exit(1) + nwGUI = GuiMain() + sys.exit(nwApp.exec_()) return diff --git a/nw/error.py b/nw/error.py index ec49dd4b..7788508f 100644 --- a/nw/error.py +++ b/nw/error.py @@ -25,56 +25,113 @@ along with this program. If not, see . """ -def formatHtmlErrMsg(exType, exValue, exTrace): - """Generates a HTML version of an exception. - """ - try: +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel, + QDialogButtonBox +) + +class NWErrorMessage(QDialog): + + def __init__(self, parent): + QDialog.__init__(self, parent=parent) + + # Widgets + self.msgIcon = QLabel() + self.msgIcon.setPixmap( + qApp.style().standardIcon(QStyle.SP_MessageBoxCritical).pixmap(64, 64) + ) + self.msgHead = QLabel() + self.msgHead.setOpenExternalLinks(True) + self.msgHead.setWordWrap(True) + + self.msgBody = QPlainTextEdit() + self.msgBody.setReadOnly(True) + + self.btnBox = QDialogButtonBox(QDialogButtonBox.Close) + self.btnBox.rejected.connect(self._doClose) + + # Assemble + self.mainBox = QGridLayout() + self.mainBox.addWidget(self.msgIcon, 0, 0, 2, 1, Qt.AlignTop) + self.mainBox.addWidget(self.msgHead, 0, 1, 1, 1, Qt.AlignTop) + self.mainBox.addWidget(self.msgBody, 1, 1, 1, 1) + self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2) + self.mainBox.setSpacing(16) + + self.setLayout(self.mainBox) + + self.setMinimumWidth(500) + self.setMinimumHeight(300) + self.setSizeGripEnabled(True) + self.resize(800, 400) + + return + + def setMessage(self, exType, exValue, exTrace): + """Generate a message and append session data, error info and + error traceback. + """ import sys from traceback import format_tb from nw import __issuesurl__, __version__ from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.QtCore import QT_VERSION_STR, QSysInfo - fmtTrace = "" - for trEntry in format_tb(exTrace): - for trLine in trEntry.split("\n"): - stripLine = trLine.lstrip(" ") - nIndent = len(trLine) - len(stripLine) - fmtTrace += " "*nIndent + stripLine + "
" + self.msgHead.setText(( + "

An unhandled error has been encountered.

" + "

Please report this error by submitting an issue report on " + "GitHub, providing a description and including the error " + "message and traceback shown below.

" + "

URL: {issueUrl}

" + ).format( + issueUrl = __issuesurl__, + )) try: kernelVersion = QSysInfo.kernelVersion() except Exception: kernelVersion = "Unknown" - theMessage = ( - "

Please report this error by submitting an issue report on " - "GitHub, providing a description and this error message. " - "URL: <{issueUrl}>.

" - "

Environment
Version: {nwVersion}, OS: {osType} ({osKernel}), " - "Python: {pyVersion} ({pyHexVer:#x}), Qt: {qtVers}, PyQt: {pyqtVers}

" - "

Error Type
{exType}: {exMessage}

" - "

Traceback
{exTrace}

" - ).format( - nwVersion = __version__, - osType = sys.platform, - osKernel = kernelVersion, - pyVersion = sys.version.split()[0], - pyHexVer = sys.hexversion, - qtVers = QT_VERSION_STR, - pyqtVers = PYQT_VERSION_STR, - issueUrl = __issuesurl__, - exType = exType.__name__, - exMessage = str(exValue), - exTrace = fmtTrace - ) + try: + self.msgBody.setPlainText(( + "Environment:\n" + "novelWriter Version: {nwVersion}\n" + "Host OS: {osType} ({osKernel})\n" + "Python: {pyVersion} ({pyHexVer:#x})\n" + "Qt: {qtVers}, PyQt: {pyqtVers}\n" + "\n" + "{exType}:\n{exMessage}\n" + "\n" + "Traceback:\n{exTrace}\n" + ).format( + nwVersion = __version__, + osType = sys.platform, + osKernel = kernelVersion, + pyVersion = sys.version.split()[0], + pyHexVer = sys.hexversion, + qtVers = QT_VERSION_STR, + pyqtVers = PYQT_VERSION_STR, + exType = exType.__name__, + exMessage = str(exValue), + exTrace = "\n".join(format_tb(exTrace)), + )) + except Exception: + self.msgBody.setPlainText("Failed to generate error report ...") - return theMessage + return - except Exception as e: - return "Could not generate error message.
%s" % str(e) + ## + # Slots + ## - return "Could not generate error message." + def _doClose(self): + """Close the dialog. + """ + self.close() + return + +# END Class NWErrorMessage def exceptionHandler(exType, exValue, exTrace): @@ -83,10 +140,10 @@ def exceptionHandler(exType, exValue, exTrace): import logging from traceback import print_tb from nw import CONFIG - from PyQt5.QtWidgets import qApp, QErrorMessage + from PyQt5.QtWidgets import qApp logger = logging.getLogger(__name__) - logger.error("%s: %s" % (exType.__name__, str(exValue))) + logger.critical("%s: %s" % (exType.__name__, str(exValue))) print_tb(exTrace) if not CONFIG.showGUI: @@ -103,14 +160,23 @@ def exceptionHandler(exType, exValue, exTrace): logger.warning("Could not find main GUI window so cannot open error dialog") return - errMsg = QErrorMessage(nwGUI) - errMsg.setWindowTitle("Unhandled Error") - errMsg.resize(800, 400) - errMsg.showMessage(( - "

An unhandled error has been encountered

%s" - ) % formatHtmlErrMsg(exType, exValue, exTrace)) + errMsg = NWErrorMessage(nwGUI) + errMsg.setMessage(exType, exValue, exTrace) + errMsg.exec_() + + try: + # Try a controlled shudown + nwGUI.closeProject(isYes=True) + nwGUI.closeMain() + logger.info("Emergency shutdown successful") + + except Exception as e: + logger.critical("Could not close the project before exiting") + logger.critical(str(e)) + + qApp.exit(1) except Exception as e: - logger.error(str(e)) + logger.critical(str(e)) return