diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 62753b57c..27932d18c 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -21,29 +21,52 @@ """The dialog which gets shown when qutebrowser crashes.""" +import re import sys import html import getpass import traceback import functools -from PyQt5.QtCore import pyqtSlot, Qt, QSize +from PyQt5.QtCore import pyqtSlot, Qt, QSize, QT_VERSION_STR from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox) +import qutebrowser from qutebrowser.utils import version, log, utils, objreg from qutebrowser.misc import miscwidgets from qutebrowser.browser.network import pastebin from qutebrowser.config import config +def parse_fatal_stacktrace(text): + """Get useful information from a fatal faulthandler stacktrace. + + Args: + text: The text to parse. + + Return: + A tuple with the first element being the error type, and the second + element being the first stacktrace frame. + """ + lines = [ + r'Fatal Python error: (.*)', + r' *', + r'Current thread [^ ]* \(most recent call first\): *', + r' File ".*", line \d+ in (.*)', + ] + m = re.match('\n'.join(lines), text) + if m is None: + # We got some invalid text. + return ('', '') + else: + return (m.group(1), m.group(2)) + + class _CrashDialog(QDialog): """Dialog which gets shown after there was a crash. - Class attributes: - NAME: The kind of condition we report. - Attributes: These are just here to have a static reference to avoid GCing. _vbox: The main QVBoxLayout @@ -57,8 +80,6 @@ class _CrashDialog(QDialog): _resolution: Whether the dialog should be accepted on close. """ - NAME = None - def __init__(self, debug, parent=None): """Constructor for CrashDialog. @@ -189,6 +210,23 @@ class _CrashDialog(QDialog): text = '\n\n'.join(chunks) self._debug_log.setText(text) + def _get_error_type(self): + """Get the type of the error we're reporting.""" + raise NotImplementedError + + def _get_paste_title_desc(self): + """Get a short description of the paste.""" + return '' + + def _get_paste_title(self): + """Get a title for the paste.""" + desc = self._get_paste_title_desc() + title = "qutebrowser {} (Qt {}) {}".format( + qutebrowser.__version__, QT_VERSION_STR, self._get_error_type()) + if desc: + title += ' - {}'.format(desc) + return title + def report(self): """Paste the crash info into the pastebin.""" lines = [] @@ -206,7 +244,7 @@ class _CrashDialog(QDialog): user = 'unknown' try: # parent: http://p.cmpl.cc/90286958 - self._paste_client.paste(user, "qutebrowser {}".format(self.NAME), + self._paste_client.paste(user, self._get_paste_title(), self._paste_text, parent='90286958') except Exception as e: log.misc.exception("Error while paste-binning") @@ -276,8 +314,6 @@ class ExceptionCrashDialog(_CrashDialog): _objects: A list of all QObjects as string. """ - NAME = 'exception' - def __init__(self, debug, pages, cmdhist, exc, objects, parent=None): self._chk_log = None super().__init__(debug, parent) @@ -329,6 +365,13 @@ class ExceptionCrashDialog(_CrashDialog): self._vbox.addWidget(info_label) self._chk_report.toggled.connect(self.on_chk_report_toggled) + def _get_error_type(self): + return 'exception' + + def _get_paste_title_desc(self): + desc = traceback.format_exception_only(self._exc[0], self._exc[1]) + return desc[0].rstrip() + def _gather_crash_info(self): self._crash_info += [ ("Exception", ''.join(traceback.format_exception(*self._exc))), @@ -363,15 +406,25 @@ class FatalCrashDialog(_CrashDialog): Attributes: _log: The log text to display. + _type: The type of error which occured. + _func: The function (top of the stack) in which the error occured. """ - NAME = 'segfault' - def __init__(self, debug, text, parent=None): super().__init__(debug, parent) self._log = text self.setAttribute(Qt.WA_DeleteOnClose) self._set_crash_info() + self._type, self._func = parse_fatal_stacktrace(self._log) + + def _get_error_type(self): + return self._type + + def _get_paste_title_desc(self): + if self._func: + return 'in {}'.format(self._func) + else: + return '' def _init_text(self): super()._init_text() @@ -408,8 +461,6 @@ class ReportDialog(_CrashDialog): _objects: A list of all QObjects as string. """ - NAME = 'report' - def __init__(self, pages, cmdhist, objects, parent=None): super().__init__(False, parent) self.setAttribute(Qt.WA_DeleteOnClose) @@ -436,6 +487,9 @@ class ReportDialog(_CrashDialog): """We don't want any checkboxes as the user wanted to report.""" pass + def _get_error_type(self): + return 'report' + def _gather_crash_info(self): super()._gather_crash_info() self._crash_info += [ diff --git a/qutebrowser/test/misc/test_crashdialog.py b/qutebrowser/test/misc/test_crashdialog.py new file mode 100644 index 000000000..c16dac44d --- /dev/null +++ b/qutebrowser/test/misc/test_crashdialog.py @@ -0,0 +1,75 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-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 . + +"""Tests for qutebrowser.misc.crashdialog.""" + +import unittest + +from qutebrowser.misc import crashdialog + + +VALID_CRASH_TEXT = """ +Fatal Python error: Segmentation fault +_ +Current thread 0x00007f09b538d700 (most recent call first): + File "", line 1 in testfunc + File "filename", line 88 in func +""" + +VALID_CRASH_TEXT_EMPTY = """ +Fatal Python error: Aborted +_ +Current thread 0x00007f09b538d700 (most recent call first): + File "", line 1 in_ + File "filename", line 88 in func +""" + +INVALID_CRASH_TEXT = """ +Hello world! +""" + + +class ParseFatalStacktraceTests(unittest.TestCase): + + """Tests for parse_fatal_stacktrace.""" + + def test_valid_text(self): + """Test parse_fatal_stacktrace with a valid text.""" + text = VALID_CRASH_TEXT.strip().replace('_', ' ') + typ, func = crashdialog.parse_fatal_stacktrace(text) + self.assertEqual(typ, "Segmentation fault") + self.assertEqual(func, 'testfunc') + + def test_valid_text(self): + """Test parse_fatal_stacktrace with a valid text but empty function.""" + text = VALID_CRASH_TEXT_EMPTY.strip().replace('_', ' ') + typ, func = crashdialog.parse_fatal_stacktrace(text) + self.assertEqual(typ, 'Aborted') + self.assertEqual(func, '') + + def test_invalid_text(self): + """Test parse_fatal_stacktrace with an invalid text.""" + text = INVALID_CRASH_TEXT.strip().replace('_', ' ') + typ, func = crashdialog.parse_fatal_stacktrace(text) + self.assertEqual(typ, '') + self.assertEqual(func, '') + + +if __name__ == '__main__': + unittest.main()