diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 2a403de5f..4c8ce2556 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -99,6 +99,9 @@ show it. *--temp-basedir*:: Use a temporary basedir. +*--no-err-windows*:: + Don't show any error windows (used for tests/smoke.py). + *--qt-name* 'NAME':: Set the window name. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 02f8af907..e75e9e4e9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -29,7 +29,7 @@ import time import shutil import tempfile -from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, QObject, Qt, QEvent) @@ -49,7 +49,7 @@ from qutebrowser.mainwindow import mainwindow from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, - objreg, usertypes, standarddir) + objreg, usertypes, standarddir, error) # We import utilcmds to run the cmdutils.register decorators. @@ -91,7 +91,7 @@ def run(args): try: sent = ipc.send_to_running_instance(args) if sent: - sys.exit(0) + sys.exit(usertypes.Exit.ok) log.init.debug("Starting IPC server...") server = ipc.IPCServer(args, qApp) objreg.register('ipc-server', server) @@ -103,14 +103,14 @@ def run(args): time.sleep(500) sent = ipc.send_to_running_instance(args) if sent: - sys.exit(0) + sys.exit(usertypes.Exit.ok) else: - ipc.display_error(e) - sys.exit(1) + ipc.display_error(e, args) + sys.exit(usertypes.Exit.err_ipc) except ipc.Error as e: - ipc.display_error(e) + ipc.display_error(e, args) # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_ipc) init(args, crash_handler) ret = qt_mainloop() @@ -144,11 +144,9 @@ def init(args, crash_handler): try: _init_modules(args, crash_handler) except (OSError, UnicodeDecodeError) as e: - msgbox = QMessageBox( - QMessageBox.Critical, "Error while initializing!", - "Error while initializing: {}".format(e)) - msgbox.exec_() - sys.exit(1) + error.handle_fatal_exc(e, args, "Error while initializing!", + pre_text="Error while initializing") + sys.exit(usertypes.Exit.err_init) QTimer.singleShot(0, functools.partial(_process_args, args)) log.init.debug("Initializing eventfilter...") @@ -398,7 +396,8 @@ def _init_modules(args, crash_handler): log.init.debug("Initializing web history...") history.init(qApp) log.init.debug("Initializing crashlog...") - crash_handler.handle_segfault() + if not args.no_err_windows: + crash_handler.handle_segfault() log.init.debug("Initializing sessions...") sessions.init(qApp) log.init.debug("Initializing js-bridge...") @@ -636,10 +635,9 @@ class Quitter: try: save_manager.save(key, is_exit=True) except OSError as e: - msgbox = QMessageBox( - QMessageBox.Critical, "Error while saving!", - "Error while saving {}: {}".format(key, e)) - msgbox.exec_() + error.handle_fatal_exc( + e, self._args, "Error while saving!", + pre_text="Error while saving {}".format(key)) # Re-enable faulthandler to stdout, then remove crash log log.destroy.debug("Deactivating crash log...") objreg.get('crash-handler').destroy_crashlogfile() diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index af5cbaf08..e195310ef 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -33,12 +33,12 @@ import collections import collections.abc from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings -from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import configdata, configexc, textwrapper from qutebrowser.config.parsers import ini, keyconf from qutebrowser.commands import cmdexc, cmdutils -from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils +from qutebrowser.utils import (message, objreg, utils, standarddir, log, + qtutils, error, usertypes) from qutebrowser.utils.usertypes import Completion @@ -137,8 +137,8 @@ def _init_main_config(parent=None): Args: parent: The parent to pass to ConfigManager. """ + args = objreg.get('args') try: - args = objreg.get('args') config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf', args.relaxed_config, parent=parent) except (configexc.Error, configparser.Error, UnicodeDecodeError) as e: @@ -149,12 +149,11 @@ def _init_main_config(parent=None): e.section, e.option) # pylint: disable=no-member except AttributeError: pass - errstr += "\n{}".format(e) - msgbox = QMessageBox(QMessageBox.Critical, - "Error while reading config!", errstr) - msgbox.exec_() + errstr += "\n" + error.handle_fatal_exc(e, args, "Error while reading config!", + pre_text=errstr) # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_config) else: objreg.register('config', config_obj) if standarddir.config() is not None: @@ -178,8 +177,8 @@ def _init_key_config(parent): Args: parent: The parent to use for the KeyConfigParser. """ + args = objreg.get('args') try: - args = objreg.get('args') key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf', args.relaxed_config, parent=parent) @@ -188,12 +187,10 @@ def _init_key_config(parent): errstr = "Error while reading key config:\n" if e.lineno is not None: errstr += "In line {}: ".format(e.lineno) - errstr += str(e) - msgbox = QMessageBox(QMessageBox.Critical, - "Error while reading key config!", errstr) - msgbox.exec_() + error.handle_fatal_exc(e, args, "Error while reading key config!", + pre_text=errstr) # We didn't really initialize much so far, so we just quit hard. - sys.exit(1) + sys.exit(usertypes.Exit.err_key_config) else: objreg.register('key-config', key_config) if standarddir.config() is not None: diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 5bb41dd1a..cdf1ebbe3 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -47,7 +47,7 @@ def check_python_version(): version_str = '.'.join(map(str, sys.version_info[:3])) text = ("At least Python 3.4 is required to run qutebrowser, but " + version_str + " is installed!\n") - if Tk: + if Tk and '--no-err-windows' not in sys.argv: root = Tk() root.withdraw() messagebox.showerror("qutebrowser: Fatal error!", text) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index bc00bce06..79a425fa9 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -584,3 +584,38 @@ class ReportErrorDialog(QDialog): btn.clicked.connect(self.close) hbox.addWidget(btn) vbox.addLayout(hbox) + + +def dump_exception_info(exc, pages, cmdhist, objects): + """Dump exception info to stderr. + + Args: + exc: An exception tuple (type, value, traceback) + pages: A list of lists of the open pages (URLs as strings) + cmdhist: A list with the command history (as strings) + objects: A list of all QObjects as string. + """ + print(file=sys.stderr) + print("\n\n===== Handling exception with --no-err-windows... =====\n\n", + file=sys.stderr) + print("\n---- Exceptions ----", file=sys.stderr) + print(''.join(traceback.format_exception(*exc)), file=sys.stderr) + print("\n---- Version info ----", file=sys.stderr) + try: + print(version.version(), file=sys.stderr) + except Exception: + traceback.print_exc() + print("\n---- Config ----", file=sys.stderr) + try: + conf = objreg.get('config') + print(conf.dump_userconfig(), file=sys.stderr) + except Exception: + traceback.print_exc() + print("\n---- Commandline args ----", file=sys.stderr) + print(' '.join(sys.argv[1:]), file=sys.stderr) + print("\n---- Open pages ----", file=sys.stderr) + print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr) + print("\n---- Command history ----", file=sys.stderr) + print('\n'.join(cmdhist), file=sys.stderr) + print("\n---- Objects ----", file=sys.stderr) + print(objects, file=sys.stderr) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 48d14a797..c860cdb6e 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -121,6 +121,7 @@ class CrashHandler(QObject): def _init_crashlogfile(self): """Start a new logfile and redirect faulthandler to it.""" + assert not self._args.no_err_windows data_dir = standarddir.data() if data_dir is None: return @@ -224,18 +225,21 @@ class CrashHandler(QObject): except TypeError: log.destroy.exception("Error while preventing shutdown") self._app.closeAllWindows() - self._crash_dialog = crashdialog.ExceptionCrashDialog( - self._args.debug, pages, cmd_history, exc, objects) - ret = self._crash_dialog.exec_() - if ret == QDialog.Accepted: # restore - self._quitter.restart(pages) + if self._args.no_err_windows: + crashdialog.dump_exception_info(exc, pages, cmd_history, objects) + else: + self._crash_dialog = crashdialog.ExceptionCrashDialog( + self._args.debug, pages, cmd_history, exc, objects) + ret = self._crash_dialog.exec_() + if ret == QDialog.Accepted: # restore + self._quitter.restart(pages) # We might risk a segfault here, but that's better than continuing to # run in some undefined state, so we only do the most needed shutdown # here. qInstallMessageHandler(None) self.destroy_crashlogfile() - sys.exit(1) + sys.exit(usertypes.Exit.exception) def raise_crashdlg(self): """Raise the crash dialog if one exists.""" diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 2307cc8c0..0416ed9a1 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -84,12 +84,15 @@ def _die(message, exception=None): print(file=sys.stderr) traceback.print_exc() app = QApplication(sys.argv) - message += '


Error:
{}'.format(exception) - msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", - message) - msgbox.setTextFormat(Qt.RichText) - msgbox.resize(msgbox.sizeHint()) - msgbox.exec_() + if '--no-err-windows' in sys.argv: + print("Exiting because of --no-err-windows.", file=sys.stderr) + else: + message += '


Error:
{}'.format(exception) + msgbox = QMessageBox(QMessageBox.Critical, "qutebrowser: Fatal error!", + message) + msgbox.setTextFormat(Qt.RichText) + msgbox.resize(msgbox.sizeHint()) + msgbox.exec_() app.quit() sys.exit(1) @@ -186,13 +189,13 @@ def check_pyqt_core(): text = text.replace('', '') text = text.replace('
', '\n') text += '\n\nError: {}'.format(e) - if tkinter: + if tkinter and '--no-err-windows' not in sys.argv: root = tkinter.Tk() root.withdraw() tkinter.messagebox.showerror("qutebrowser: Fatal error!", text) else: print(text, file=sys.stderr) - if '--debug' in sys.argv: + if '--debug' in sys.argv or '--no-err-windows' in sys.argv: print(file=sys.stderr) traceback.print_exc() sys.exit(1) diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index ba0ec87c9..749eb4e4e 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -27,9 +27,8 @@ import hashlib from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket -from PyQt5.QtWidgets import QMessageBox -from qutebrowser.utils import log, usertypes +from qutebrowser.utils import log, usertypes, error CONNECT_TIMEOUT = 100 @@ -130,12 +129,12 @@ class IPCServer(QObject): self._socketname)) @pyqtSlot(int) - def on_error(self, error): + def on_error(self, err): """Convenience method which calls _socket_error on an error.""" self._timer.stop() log.ipc.debug("Socket error {}: {}".format( self._socket.error(), self._socket.errorString())) - if error != QLocalSocket.PeerClosedError: + if err != QLocalSocket.PeerClosedError: _socket_error("handling IPC connection", self._socket) @pyqtSlot() @@ -282,9 +281,8 @@ def send_to_running_instance(args): return False -def display_error(exc): +def display_error(exc, args): """Display a message box with an IPC error.""" - text = '{}\n\nMaybe another instance is running but frozen?'.format(exc) - msgbox = QMessageBox(QMessageBox.Critical, "Error while connecting to " - "running instance!", text) - msgbox.exec_() + error.handle_fatal_exc( + exc, args, "Error while connecting to running instance!", + post_text="Maybe another instance is running but frozen?") diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 03f3c582b..b2d61fb48 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -96,6 +96,8 @@ def get_argparser(): help="Drop into pdb on exceptions.") debug.add_argument('--temp-basedir', action='store_true', help="Use a " "temporary basedir.") + debug.add_argument('--no-err-windows', action='store_true', help="Don't " + "show any error windows (used for tests/smoke.py).") # For the Qt args, we use store_const with const=True rather than # store_true because we want the default to be None, to make # utils.qt:get_args easier. diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py new file mode 100644 index 000000000..d637f4d33 --- /dev/null +++ b/qutebrowser/utils/error.py @@ -0,0 +1,54 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser 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. +# +# qutebrowser 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 qutebrowser. If not, see . + +"""Tools related to error printing/displaying.""" + +from PyQt5.QtWidgets import QMessageBox + +from qutebrowser.utils import log + + +def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''): + """Handle a fatal "expected" exception by displaying an error box. + + If --no-err-windows is given as argument, the text is logged to the error + logger instead. + + Args: + exc: The Exception object being handled. + args: The argparser namespace. + title: The title to be used for the error message. + pre_text: The text to be displayed before the exception text. + post_text: The text to be displayed after the exception text. + """ + if args.no_err_windows: + log.misc.exception("Handling fatal {} with --no-err-windows!".format( + exc.__class__.__name__)) + log.misc.error("title: {}".format(title)) + log.misc.error("pre_text: {}".format(pre_text)) + log.misc.error("post_text: {}".format(post_text)) + else: + if pre_text: + msg_text = '{}: {}'.format(pre_text, exc) + else: + msg_text = str(exc) + if post_text: + msg_text += '\n\n{}'.format(post_text) + msgbox = QMessageBox(QMessageBox.Critical, title, msg_text) + msgbox.exec_() diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 7371f3114..28a30f941 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -240,6 +240,11 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value', 'quickmark_by_name', 'url', 'sessions']) +# Exit statuses for errors +Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', + 'err_config', 'err_key_config']) + + class Question(QObject): """A question asked to the user, e.g. via the status bar. diff --git a/tox.ini b/tox.ini index e2bdf35f7..28b0c4bf9 100644 --- a/tox.ini +++ b/tox.ini @@ -112,6 +112,13 @@ commands = git --no-pager diff --exit-code --stat {envpython} scripts/asciidoc2html.py {posargs} +[testenv:smoke] +deps = + -rrequirements.txt +commands = + {[testenv:mkvenv]commands} + {envpython} -m qutebrowser --debug --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" + [pytest] norecursedirs = .tox .venv markers =