qutebrowser/qutebrowser/widgets/crash.py

430 lines
15 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2014-02-10 15:01:05 +01:00
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
# pylint: disable=broad-except
2014-02-10 15:01:05 +01:00
"""The dialog which gets shown when qutebrowser crashes."""
import sys
2014-10-26 19:14:46 +01:00
import html
2014-10-31 07:57:50 +01:00
import getpass
2014-02-10 15:01:05 +01:00
import traceback
2014-10-26 19:14:46 +01:00
import functools
2014-02-10 15:01:05 +01:00
2014-10-26 19:14:46 +01:00
from PyQt5.QtCore import pyqtSlot, Qt, QSize
2014-02-17 12:23:52 +01:00
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
2014-10-26 19:14:46 +01:00
QVBoxLayout, QHBoxLayout, QCheckBox)
2014-02-10 15:01:05 +01:00
2014-09-24 07:06:45 +02:00
from qutebrowser.utils import version, log, utils, objreg
2014-10-26 19:14:46 +01:00
from qutebrowser.widgets.misc import DetailFold
2014-02-10 15:01:05 +01:00
class _CrashDialog(QDialog):
2014-02-10 15:01:05 +01:00
2014-02-18 16:38:13 +01:00
"""Dialog which gets shown after there was a crash.
2014-02-10 15:01:05 +01:00
2014-10-31 07:57:50 +01:00
Class attributes:
NAME: The kind of condition we report.
2014-02-18 16:38:13 +01:00
Attributes:
These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout
_lbl: The QLabel with the static text
2014-10-26 19:14:46 +01:00
_debug_log: The QTextEdit with the crash information
2014-02-18 16:38:13 +01:00
_hbox: The QHboxLayout containing the buttons
2014-05-06 15:36:15 +02:00
_url: Pastebin URL QLabel.
_crash_info: A list of tuples with title and crash information.
2014-02-18 16:38:13 +01:00
"""
2014-10-31 07:57:50 +01:00
NAME = None
2014-10-31 07:05:04 +01:00
def __init__(self, debug, parent=None):
"""Constructor for CrashDialog.
Args:
debug: Whether --debug was given.
"""
2014-06-17 07:17:21 +02:00
super().__init__(parent)
2014-06-27 07:39:01 +02:00
# We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects.
2014-10-26 19:14:46 +01:00
self._buttons = []
self._crash_info = []
self._hbox = None
self._lbl = None
2014-10-26 19:14:46 +01:00
self._chk_report = None
2014-04-25 16:53:23 +02:00
self.setWindowTitle("Whoops!")
2014-10-26 19:14:46 +01:00
self.resize(QSize(640, 600))
2014-02-18 16:38:13 +01:00
self._vbox = QVBoxLayout(self)
self._init_text()
2014-10-26 19:14:46 +01:00
info = QLabel("What were you doing when this crash/bug happened?")
self._vbox.addWidget(info)
2014-10-31 07:13:17 +01:00
self._info = QTextEdit(tabChangesFocus=True, acceptRichText=False)
2014-10-26 19:14:46 +01:00
self._info.setPlaceholderText("- Opened http://www.example.com/\n"
"- Switched tabs\n"
"- etc...")
self._vbox.addWidget(self._info, 5)
contact = QLabel("How can I contact you if I need more info?")
self._vbox.addWidget(contact)
2014-10-31 07:13:17 +01:00
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
2014-10-26 19:14:46 +01:00
self._contact.setPlaceholderText("Github username, mail or IRC")
self._vbox.addWidget(self._contact, 2)
self._vbox.addSpacing(15)
2014-10-31 07:13:17 +01:00
self._debug_log = QTextEdit(tabChangesFocus=True, acceptRichText=False,
lineWrapMode=QTextEdit.NoWrap)
2014-10-31 07:05:04 +01:00
self._debug_log.hide()
2014-10-26 19:14:46 +01:00
info = QLabel("<i>You can edit the log below to remove sensitive "
"information.</i>", wordWrap=True)
info.hide()
self._fold = DetailFold("Show log", self)
self._fold.toggled.connect(self._debug_log.setVisible)
self._fold.toggled.connect(info.setVisible)
2014-10-31 07:05:04 +01:00
if debug:
self._fold.toggle()
2014-10-26 19:14:46 +01:00
self._vbox.addWidget(self._fold)
self._vbox.addWidget(info)
self._vbox.addWidget(self._debug_log, 10)
self._vbox.addSpacing(15)
2014-10-31 07:05:04 +01:00
self._init_checkboxes(debug)
self._init_buttons()
2014-05-06 15:36:15 +02:00
2014-06-25 23:03:33 +02:00
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self)
2014-06-25 23:03:33 +02:00
def _init_text(self):
"""Initialize the main text to be displayed on an exception.
Should be extended by superclass to set the actual text."""
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
textInteractionFlags=Qt.LinksAccessibleByMouse)
self._vbox.addWidget(self._lbl)
2014-10-31 07:05:04 +01:00
def _init_checkboxes(self, debug):
2014-10-26 19:14:46 +01:00
"""Initialize the checkboxes.
2014-10-31 07:05:04 +01:00
Args:
debug: Whether a --debug arg was given.
2014-10-26 19:14:46 +01:00
"""
self._chk_report = QCheckBox("Send a report")
2014-10-31 07:05:04 +01:00
if not debug:
self._chk_report.setChecked(True)
2014-10-26 19:14:46 +01:00
self._vbox.addWidget(self._chk_report)
info_label = QLabel("<i>Note that without your help, I can't fix the "
"bug you encountered.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
def _init_buttons(self):
"""Initialize the buttons.
2014-10-26 19:14:46 +01:00
Should be extended by subclasses to provide the actual buttons.
"""
2014-06-19 14:13:44 +02:00
self._hbox = QHBoxLayout()
2014-02-18 16:38:13 +01:00
self._vbox.addLayout(self._hbox)
2014-06-19 14:13:44 +02:00
self._hbox.addStretch()
2014-02-13 09:10:53 +01:00
def _gather_crash_info(self):
2014-02-19 10:58:32 +01:00
"""Gather crash information to display.
Args:
pages: A list of lists of the open pages (URLs as strings)
2014-02-19 10:58:32 +01:00
cmdhist: A list with the command history (as strings)
exc: An exception tuple (type, value, traceback)
"""
try:
self._crash_info.append(("Version info", version.version()))
except Exception:
self._crash_info.append(("Version info", traceback.format_exc()))
try:
2014-09-24 07:06:45 +02:00
conf = objreg.get('config')
2014-09-23 23:17:36 +02:00
self._crash_info.append(("Config", conf.dump_userconfig()))
except Exception:
self._crash_info.append(("Config", traceback.format_exc()))
2014-10-26 19:14:46 +01:00
def _set_crash_info(self):
"""Set/update the crash info."""
self._crash_info = []
self._gather_crash_info()
chunks = []
for (header, body) in self._crash_info:
2014-03-06 00:06:28 +01:00
if body is not None:
h = '==== {} ===='.format(header)
chunks.append('\n'.join([h, body]))
2014-10-26 19:14:46 +01:00
text = '\n\n'.join(chunks)
self._debug_log.setText(text)
2014-05-06 15:36:15 +02:00
2014-10-26 19:14:46 +01:00
def report(self):
2014-05-06 15:36:15 +02:00
"""Paste the crash info into the pastebin."""
2014-10-26 19:14:46 +01:00
lines = []
lines.append("========== Report ==========")
lines.append(self._info.toPlainText())
lines.append("========== Contact ==========")
lines.append(self._contact.toPlainText())
lines.append("========== Debug log ==========")
lines.append(self._debug_log.toPlainText())
text = '\n\n'.join(lines)
2014-05-06 15:36:15 +02:00
try:
2014-10-31 07:57:50 +01:00
user = getpass.getuser()
except Exception as e:
log.misc.exception("Error while getting user")
user = 'unknown'
try:
utils.pastebin(user, "qutebrowser {}".format(self.NAME), text,
parent='90286958') # http://p.cmpl.cc/90286958
except Exception as e:
log.misc.exception("Error while paste-binning")
2014-10-26 19:14:46 +01:00
exc_text = '{}: {}'.format(e.__class__.__name__, e)
error_dlg = ReportErrorDialog(exc_text, text, self)
error_dlg.exec_()
@pyqtSlot()
def on_button_clicked(self, button, accept):
"""Report and close dialog if button was clicked."""
button.setText("Reporting...")
for btn in self._buttons:
btn.setEnabled(False)
self.hide()
2014-10-26 19:14:46 +01:00
self.maybe_report()
if accept:
self.accept()
else:
self.reject()
@pyqtSlot()
def maybe_report(self):
"""Report the bug if the user allowed us to."""
if self._chk_report.isChecked():
self.report()
class ExceptionCrashDialog(_CrashDialog):
"""Dialog which gets shown on an exception.
Attributes:
2014-10-26 19:14:46 +01:00
_buttons: A list of buttons.
_pages: A list of lists of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings)
_exc: An exception tuple (type, value, traceback)
2014-06-17 23:04:58 +02:00
_objects: A list of all QObjects as string.
"""
2014-10-31 07:57:50 +01:00
NAME = 'exception'
2014-10-31 07:05:04 +01:00
def __init__(self, debug, pages, cmdhist, exc, objects, parent=None):
super().__init__(debug, parent)
self._pages = pages
self._cmdhist = cmdhist
self._exc = exc
2014-06-17 23:04:58 +02:00
self._objects = objects
self.setModal(True)
2014-10-26 19:14:46 +01:00
self._set_crash_info()
def _init_text(self):
super()._init_text()
2014-10-26 19:14:46 +01:00
text = "<b>Argh! qutebrowser crashed unexpectedly.</b>"
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
2014-10-26 19:14:46 +01:00
btn_quit = QPushButton("Quit")
btn_quit.clicked.connect(
functools.partial(self.on_button_clicked, btn_quit, False))
self._hbox.addWidget(btn_quit)
btn_restart = QPushButton("Restart", default=True)
btn_restart.clicked.connect(
functools.partial(self.on_button_clicked, btn_restart, True))
self._hbox.addWidget(btn_restart)
self._buttons = [btn_quit, btn_restart]
2014-10-31 07:05:04 +01:00
def _init_checkboxes(self, debug):
2014-10-26 19:14:46 +01:00
"""Add checkboxes to send crash report."""
2014-10-31 07:05:04 +01:00
super()._init_checkboxes(debug)
2014-10-26 19:14:46 +01:00
self._chk_log = QCheckBox("Include a debug log and a list of open "
"pages", checked=True)
2014-10-31 07:05:04 +01:00
if debug:
self._chk_log.setChecked(False)
self._chk_log.setEnabled(False)
2014-10-26 19:14:46 +01:00
self._chk_log.toggled.connect(self._set_crash_info)
self._vbox.addWidget(self._chk_log)
info_label = QLabel("<i>This makes it a lot easier to diagnose the "
"crash, but it might contain sensitive "
"information such as which pages you visited "
"or keyboard input.</i>", wordWrap=True)
self._vbox.addWidget(info_label)
self._chk_report.toggled.connect(self.on_chk_report_toggled)
def _gather_crash_info(self):
self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))),
]
2014-10-26 19:14:46 +01:00
if self._chk_log.isChecked():
self._crash_info += [
("Commandline args", ' '.join(sys.argv[1:])),
("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects),
]
try:
self._crash_info.append(
("Debug log", log.ram_handler.dump_log()))
except Exception:
self._crash_info.append(
("Debug log", traceback.format_exc()))
super()._gather_crash_info()
@pyqtSlot()
def on_chk_report_toggled(self):
"""Disable log checkbox if report is disabled."""
is_checked = self._chk_report.isChecked()
self._chk_log.setEnabled(is_checked)
self._chk_log.setChecked(is_checked)
class FatalCrashDialog(_CrashDialog):
"""Dialog which gets shown when a fatal error occured.
Attributes:
_log: The log text to display.
"""
2014-10-31 07:57:50 +01:00
NAME = 'segfault'
2014-10-31 07:05:04 +01:00
def __init__(self, debug, text, parent=None):
super().__init__(debug, parent)
2014-10-26 19:14:46 +01:00
self._log = text
2014-06-27 07:39:01 +02:00
self.setAttribute(Qt.WA_DeleteOnClose)
2014-10-26 19:14:46 +01:00
self._set_crash_info()
def _init_text(self):
super()._init_text()
text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>"
"<br/>Note: Crash reports for fatal crashes sometimes don't "
"contain the information necessary to fix an issue. Please "
"follow the steps in <a href='https://github.com/The-Compiler/"
"qutebrowser/blob/master/doc/stacktrace.asciidoc'>"
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
2014-10-26 19:14:46 +01:00
btn_ok = QPushButton(text="OK", default=True)
btn_ok.clicked.connect(
functools.partial(self.on_button_clicked, btn_ok, True))
self._hbox.addWidget(btn_ok)
self._buttons = [btn_ok]
def _gather_crash_info(self):
self._crash_info += [
("Fault log", self._log),
]
2014-10-26 19:14:46 +01:00
super()._gather_crash_info()
2014-06-25 22:22:30 +02:00
class ReportDialog(_CrashDialog):
"""Dialog which gets shown when the user wants to report an issue by hand.
Attributes:
_pages: A list of the open pages (URLs as strings)
_cmdhist: A list with the command history (as strings)
_objects: A list of all QObjects as string.
"""
2014-10-31 07:57:50 +01:00
NAME = 'report'
def __init__(self, pages, cmdhist, objects, parent=None):
2014-10-31 07:05:04 +01:00
super().__init__(False, parent)
2014-10-26 19:14:46 +01:00
self.setAttribute(Qt.WA_DeleteOnClose)
self._btn_report = None
2014-06-25 22:22:30 +02:00
self._pages = pages
self._cmdhist = cmdhist
self._objects = objects
2014-10-26 19:14:46 +01:00
self._set_crash_info()
2014-06-25 22:22:30 +02:00
def _init_text(self):
super()._init_text()
2014-10-26 19:14:46 +01:00
text = "Please describe the bug you encountered below."
2014-06-25 22:22:30 +02:00
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
2014-10-26 19:14:46 +01:00
self._btn_report = QPushButton("Report", default=True)
self._btn_report.clicked.connect(self.report)
self._btn_report.clicked.connect(self.close)
self._hbox.addWidget(self._btn_report)
2014-10-31 07:05:04 +01:00
def _init_checkboxes(self, _debug):
2014-10-26 19:14:46 +01:00
"""We don't want any checkboxes as the user wanted to report."""
pass
2014-06-25 22:22:30 +02:00
def _gather_crash_info(self):
super()._gather_crash_info()
self._crash_info += [
("Commandline args", ' '.join(sys.argv[1:])),
("Open Pages", '\n\n'.join('\n'.join(e) for e in self._pages)),
2014-06-25 22:22:30 +02:00
("Command history", '\n'.join(self._cmdhist)),
("Objects", self._objects),
]
try:
2014-08-26 19:10:14 +02:00
self._crash_info.append(("Debug log", log.ram_handler.dump_log()))
except Exception:
self._crash_info.append(("Debug log", traceback.format_exc()))
2014-10-26 19:14:46 +01:00
@pyqtSlot()
def maybe_report(self):
"""Report the crash.
We don't have a "Send a report" checkbox here because it was a manual
report, which would be pretty useless without this info.
"""
self.report()
class ReportErrorDialog(QDialog):
"""An error dialog shown on unsuccessful reports."""
def __init__(self, exc_text, text, parent=None):
super().__init__(parent)
vbox = QVBoxLayout(self)
label = QLabel("<b>There was an error while reporting the crash</b>:"
"<br/>{}<br/><br/>"
"Please copy the text below and send a mail to "
"<a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> - Thanks!".format(
html.escape(exc_text)))
vbox.addWidget(label)
2014-10-31 07:13:17 +01:00
txt = QTextEdit(readOnly=True, tabChangesFocus=True,
acceptRichText=False)
2014-10-26 19:14:46 +01:00
txt.setText(text)
txt.selectAll()
vbox.addWidget(txt)
hbox = QHBoxLayout()
hbox.addStretch()
btn = QPushButton("Close")
btn.clicked.connect(self.close)
hbox.addWidget(btn)
vbox.addLayout(hbox)