""" novelWriter – Exception Handling ================================ File History: Created: 2020-08-02 [0.10.2] This file is a part of novelWriter Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # noqa from __future__ import annotations import logging import random import sys from typing import TYPE_CHECKING from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtGui import QFont, QFontDatabase from PyQt6.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QGridLayout, QLabel, QPlainTextEdit, QStyle, QWidget ) if TYPE_CHECKING: from types import TracebackType logger = logging.getLogger(__name__) def logException() -> None: """Log the content of an exception message.""" exType, exValue, _ = sys.exc_info() if exType is not None: logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2) def formatException(exc: BaseException) -> str: """Format an exception as a string the same way the default exception handler does. """ return f"{type(exc).__name__}: {exc!s}" class NWErrorMessage(QDialog): """GUI: Error Dialog.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self.setObjectName("NWErrorMessage") # Widgets self.msgIcon = QLabel() if style := QApplication.style(): self.msgIcon.setPixmap( style.standardIcon(QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(64, 64) ) self.msgHead = QLabel() self.msgHead.setOpenExternalLinks(True) self.msgHead.setWordWrap(True) font = QFont() font.setPointSize(round(0.9*self.font().pointSize())) font.setFamily(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont).family()) self.msgBody = QPlainTextEdit() self.msgBody.setFont(font) self.msgBody.setReadOnly(True) self.btnBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) self.btnBox.rejected.connect(self._doClose) # Assemble self.mainBox = QGridLayout() self.mainBox.addWidget(self.msgIcon, 0, 0, 2, 1, Qt.AlignmentFlag.AlignTop) self.mainBox.addWidget(self.msgHead, 0, 1, 1, 1, Qt.AlignmentFlag.AlignTop) self.mainBox.addWidget(self.msgBody, 1, 1, 1, 1) self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2) self.mainBox.setSpacing(16) # Pick a random window title from a set of error messages by # Hex the computer, Unseen University, Ankh-Morpork, Discworld self.setWindowTitle([ "+++ Out of Cheese Error +++", "+++ Divide by Cucumber Error +++", "+++ Whoops! Here Comes The Cheese! +++", "+++ Please Reinstall Universe and Reboot +++", "+++ Error At Address 14, Treacle Mine Road +++", ][random.randint(0, 4)]) self.setLayout(self.mainBox) self.setMinimumWidth(500) self.setMinimumHeight(300) self.setSizeGripEnabled(True) self.resize(800, 400) def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None: """Generate a message and append session data, error info and error traceback. """ from traceback import format_tb from PyQt6.QtCore import PYQT_VERSION_STR, QT_VERSION_STR, QSysInfo from novelwriter import __version__ from novelwriter.constants import nwConst 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.

" f"

URL: {nwConst.URL_REPORT}

" ) try: kernelVersion = QSysInfo.kernelVersion() except Exception: kernelVersion = "Unknown" try: import enchant enchantVersion = enchant.__version__ except Exception: enchantVersion = "Unknown" try: trace = "\n".join(format_tb(exTrace)) self.msgBody.setPlainText( "Environment:\n" f"novelWriter Version: {__version__}\n" f"Host OS: {sys.platform} ({kernelVersion})\n" f"Python: {sys.version.split()[0]} ({sys.hexversion:#x})\n" f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"enchant: {enchantVersion}\n\n" f"{exType.__name__}:\n{exValue!s}\n\n" f"Traceback:\n{trace}\n" ) except Exception: self.msgBody.setPlainText("Failed to generate error report ...") ## # Slots ## @pyqtSlot() def _doClose(self) -> None: """Close the dialog.""" self.close() def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None: """Catch unhandled global exceptions.""" from traceback import print_tb from PyQt6.QtWidgets import QApplication logger.critical("%s: %s", exType.__name__, exValue) print_tb(exTrace) try: nwGUI = None for qWin in QApplication.topLevelWidgets(): if qWin.objectName() == "GuiMain": nwGUI = qWin break if nwGUI is None: logger.warning("Could not find main GUI window so cannot open error dialog") return errMsg = NWErrorMessage(nwGUI) errMsg.setMessage(exType, exValue, exTrace) errMsg.exec() try: # Try a controlled shutdown nwGUI.closeProject(isYes=True) # type: ignore nwGUI.closeMain() # type: ignore logger.info("Emergency shutdown successful") except Exception as exc: logger.critical("Could not close the project before exiting") logger.critical(formatException(exc)) QApplication.exit(1) except Exception as exc: logger.critical(formatException(exc)) return