Start new crash dialog

This commit is contained in:
Florian Bruhin 2014-10-26 19:14:46 +01:00
parent 904d84db7e
commit da0f433260
2 changed files with 290 additions and 136 deletions

View File

@ -22,14 +22,16 @@
"""The dialog which gets shown when qutebrowser crashes."""
import sys
import html
import traceback
import getpass
import functools
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout)
QVBoxLayout, QHBoxLayout, QCheckBox)
from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.widgets.misc import DetailFold
class _CrashDialog(QDialog):
@ -40,41 +42,55 @@ class _CrashDialog(QDialog):
These are just here to have a static reference to avoid GCing.
_vbox: The main QVBoxLayout
_lbl: The QLabel with the static text
_txt: The QTextEdit with the crash information
_debug_log: The QTextEdit with the crash information
_hbox: The QHboxLayout containing the buttons
_url: Pastebin URL QLabel.
_crash_info: A list of tuples with title and crash information.
Class attributes:
CRASHTEXT: The text to be displayed in the dialog.
"""
CRASHTEXT = ("Please review and edit the info below, then either submit "
"it to <a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> or click 'Report'.<br/><br/>"
"<i>Note that without your help, I can't fix the bug you "
"encountered. With the report, I most probably will."
"</i><br/><br/>")
def __init__(self, parent=None):
"""Constructor for CrashDialog."""
super().__init__(parent)
# We don't set WA_DeleteOnClose here as on an exception, we'll get
# closed anyways, and it only could have unintended side-effects.
self._crash_info = None
self._buttons = []
self._crash_info = []
self._hbox = None
self._lbl = None
self._gather_crash_info()
self._chk_report = None
self.setWindowTitle("Whoops!")
self.resize(QSize(800, 600))
self.resize(QSize(640, 600))
self._vbox = QVBoxLayout(self)
self._init_text()
self._txt = QTextEdit()
self._txt.setText(self._format_crash_info())
self._vbox.addWidget(self._txt)
self._url = QLabel()
self._set_text_flags(self._url)
self._vbox.addWidget(self._url)
info = QLabel("What were you doing when this crash/bug happened?")
self._vbox.addWidget(info)
self._info = QTextEdit(tabChangesFocus=True)
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)
self._contact = QTextEdit(tabChangesFocus=True)
self._contact.setPlaceholderText("Github username, mail or IRC")
self._vbox.addWidget(self._contact, 2)
self._vbox.addSpacing(15)
self._debug_log = QTextEdit(tabChangesFocus=True)
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)
self._vbox.addWidget(self._fold)
self._vbox.addWidget(info)
self._vbox.addWidget(self._debug_log, 10)
self._debug_log.hide()
self._vbox.addSpacing(15)
self._init_checkboxes()
self._init_buttons()
def __repr__(self):
@ -86,29 +102,29 @@ class _CrashDialog(QDialog):
Should be extended by superclass to set the actual text."""
self._lbl = QLabel()
self._lbl.setWordWrap(True)
self._set_text_flags(self._lbl)
self._vbox.addWidget(self._lbl)
def _init_checkboxes(self):
"""Initialize the checkboxes.
Should be overwritten by subclasses.
"""
self._chk_report = QCheckBox("Send a report")
self._chk_report.setChecked(True)
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.
Should be extended by superclass to provide the actual buttons.
Should be extended by subclasses to provide the actual buttons.
"""
self._hbox = QHBoxLayout()
self._vbox.addLayout(self._hbox)
self._hbox.addStretch()
def _set_text_flags(self, obj):
"""Set text interaction flags of a widget to allow link clicking.
Args:
obj: A QLabel.
"""
obj.setTextInteractionFlags(Qt.TextSelectableByMouse |
Qt.TextSelectableByKeyboard |
Qt.LinksAccessibleByMouse |
Qt.LinksAccessibleByKeyboard)
def _gather_crash_info(self):
"""Gather crash information to display.
@ -117,14 +133,6 @@ class _CrashDialog(QDialog):
cmdhist: A list with the command history (as strings)
exc: An exception tuple (type, value, traceback)
"""
self._crash_info = [
("How did it happen?", ""),
]
try:
self._crash_info.append(("Contact info",
"User: {}".format(getpass.getuser())))
except Exception:
self._crash_info.append(("Contact info", traceback.format_exc()))
try:
self._crash_info.append(("Version info", version.version()))
except Exception:
@ -135,33 +143,53 @@ class _CrashDialog(QDialog):
except Exception:
self._crash_info.append(("Config", traceback.format_exc()))
def _format_crash_info(self):
"""Format the gathered crash info to be displayed.
Return:
The string to display.
"""
chunks = ["Please edit this report to remove sensitive info, and add "
"as much info as possible about how it happened.\n"
"If it's okay if I contact you about this bug report, "
"please also add your contact info (Mail/IRC/Jabber)."]
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:
if body is not None:
h = '==== {} ===='.format(header)
chunks.append('\n'.join([h, body]))
return '\n\n'.join(chunks)
text = '\n\n'.join(chunks)
self._debug_log.setText(text)
def pastebin(self):
def report(self):
"""Paste the crash info into the pastebin."""
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)
try:
url = utils.pastebin(self._txt.toPlainText())
utils.pastebin(text)
except Exception as e:
log.misc.exception("Error while paste-binning")
self._url.setText('Error while reporting: {}: {}'.format(
e.__class__.__name__, e))
return
self._btn_pastebin.setEnabled(False)
self._url.setText("Reported to: <a href='{}'>{}</a>".format(url, url))
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.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):
@ -169,9 +197,7 @@ class ExceptionCrashDialog(_CrashDialog):
"""Dialog which gets shown on an exception.
Attributes:
_btn_quit: The quit button
_btn_restore: the restore button
_btn_pastebin: the pastebin button
_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)
@ -179,55 +205,72 @@ class ExceptionCrashDialog(_CrashDialog):
"""
def __init__(self, pages, cmdhist, exc, objects, parent=None):
super().__init__(parent)
self._pages = pages
self._cmdhist = cmdhist
self._exc = exc
self._btn_quit = None
self._btn_restore = None
self._btn_pastebin = None
self._objects = objects
super().__init__(parent)
self.setModal(True)
self._set_crash_info()
def _init_text(self):
super()._init_text()
text = ("<b>Argh! qutebrowser crashed unexpectedly.</b><br/><br/>" +
self.CRASHTEXT)
if self._pages:
text += ("You can click 'Restore tabs' after reporting to attempt "
"to reopen your open tabs.")
text = "<b>Argh! qutebrowser crashed unexpectedly.</b>"
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
self._btn_quit = QPushButton()
self._btn_quit.setText("Quit")
self._btn_quit.clicked.connect(self.reject)
self._hbox.addWidget(self._btn_quit)
if self._pages:
self._btn_restore = QPushButton()
self._btn_restore.setText("Restore tabs")
self._btn_restore.clicked.connect(self.accept)
self._hbox.addWidget(self._btn_restore)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Report")
self._btn_pastebin.clicked.connect(self.pastebin)
self._btn_pastebin.setDefault(True)
self._hbox.addWidget(self._btn_pastebin)
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]
def _init_checkboxes(self):
"""Add checkboxes to send crash report."""
super()._init_checkboxes()
self._chk_log = QCheckBox("Include a debug log and a list of open "
"pages", checked=True)
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):
super()._gather_crash_info()
self._crash_info += [
("Exception", ''.join(traceback.format_exception(*self._exc))),
("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()))
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):
@ -236,40 +279,32 @@ class FatalCrashDialog(_CrashDialog):
Attributes:
_log: The log text to display.
_btn_ok: The OK button.
_btn_pastebin: The pastebin button.
"""
def __init__(self, text, parent=None):
self._log = text
self._btn_ok = None
self._btn_pastebin = None
super().__init__(parent)
self._log = text
self.setAttribute(Qt.WA_DeleteOnClose)
self._set_crash_info()
def _init_text(self):
super()._init_text()
text = ("<b>qutebrowser was restarted after a fatal crash.</b><br/>"
"<br/>" + self.CRASHTEXT)
text = "<b>qutebrowser was restarted after a fatal crash.</b>"
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
self._btn_ok = QPushButton()
self._btn_ok.setText("OK")
self._btn_ok.clicked.connect(self.accept)
self._hbox.addWidget(self._btn_ok)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Report")
self._btn_pastebin.clicked.connect(self.pastebin)
self._btn_pastebin.setDefault(True)
self._hbox.addWidget(self._btn_pastebin)
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):
super()._gather_crash_info()
self._crash_info += [
("Fault log", self._log),
]
super()._gather_crash_info()
class ReportDialog(_CrashDialog):
@ -277,40 +312,35 @@ class ReportDialog(_CrashDialog):
"""Dialog which gets shown when the user wants to report an issue by hand.
Attributes:
_btn_ok: The OK button.
_btn_pastebin: The pastebin button.
_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.
"""
def __init__(self, pages, cmdhist, objects, parent=None):
self._pages = pages
self._cmdhist = cmdhist
self._btn_ok = None
self._btn_pastebin = None
self._objects = objects
super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self._btn_report = None
self._pages = pages
self._cmdhist = cmdhist
self._objects = objects
self._set_crash_info()
def _init_text(self):
super()._init_text()
text = ("Please describe the bug you encountered below, then either "
"submit it to <a href='mailto:crash@qutebrowser.org'>"
"crash@qutebrowser.org</a> or click 'Report'.")
text = "Please describe the bug you encountered below."
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
self._btn_ok = QPushButton()
self._btn_ok.setText("OK")
self._btn_ok.clicked.connect(self.accept)
self._hbox.addWidget(self._btn_ok)
self._btn_pastebin = QPushButton()
self._btn_pastebin.setText("Report")
self._btn_pastebin.clicked.connect(self.pastebin)
self._btn_pastebin.setDefault(True)
self._hbox.addWidget(self._btn_pastebin)
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)
def _init_checkboxes(self):
"""We don't want any checkboxes as the user wanted to report."""
pass
def _gather_crash_info(self):
super()._gather_crash_info()
@ -324,3 +354,39 @@ class ReportDialog(_CrashDialog):
self._crash_info.append(("Debug log", log.ram_handler.dump_log()))
except Exception:
self._crash_info.append(("Debug log", traceback.format_exc()))
@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)
txt = QTextEdit(readOnly=True, tabChangesFocus=True)
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)

View File

@ -19,9 +19,10 @@
"""Misc. widgets used at different places."""
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWidgets import QLineEdit, QApplication
from PyQt5.QtGui import QValidator, QClipboard
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize
from PyQt5.QtWidgets import (QLineEdit, QApplication, QWidget, QHBoxLayout,
QLabel, QStyleOption, QStyle)
from PyQt5.QtGui import QValidator, QClipboard, QPainter
from qutebrowser.models import cmdhistory
from qutebrowser.utils import utils
@ -135,3 +136,90 @@ class _CommandValidator(QValidator):
return (QValidator.Acceptable, string, pos)
else:
return (QValidator.Invalid, string, pos)
class DetailFold(QWidget):
"""A "fold" widget with an arrow to show/hide details.
Attributes:
_folded: Whether the widget is currently folded or not.
_hbox: The HBoxLayout the arrow/label are in.
_arrow: The FoldArrow widget.
Signals:
toggled: Emitted when the widget was folded/unfolded.
arg 0: bool, if the contents are currently visible.
"""
toggled = pyqtSignal(bool)
def __init__(self, text, parent=None):
super().__init__(parent)
self._folded = True
self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0)
self._arrow = _FoldArrow()
self._hbox.addWidget(self._arrow)
label = QLabel(text)
self._hbox.addWidget(label)
self._hbox.addStretch()
def toggle(self):
"""Toggle the fold of the widget."""
self._folded = not self._folded
self._arrow.fold(self._folded)
self.toggled.emit(not self._folded)
def mousePressEvent(self, e):
"""Toggle the fold if the widget was pressed.
Args:
e: The QMouseEvent.
"""
if e.button() == Qt.LeftButton:
e.accept()
self.toggle()
else:
super().mousePressEvent(e)
class _FoldArrow(QWidget):
"""The arrow shown for the DetailFold widget.
Attributes:
_folded: Whether the widget is currently folded or not.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._folded = True
def fold(self, folded):
"""Fold/unfold the widget.
Args:
folded: The new desired state.
"""
self._folded = folded
self.update()
def paintEvent(self, _event):
"""Paint the arrow.
Args:
_paint: The QPaintEvent (unused).
"""
opt = QStyleOption()
opt.initFrom(self)
painter = QPainter(self)
if self._folded:
elem = QStyle.PE_IndicatorArrowRight
else:
elem = QStyle.PE_IndicatorArrowDown
self.style().drawPrimitive(elem, opt, painter, self)
def minimumSizeHint(self):
"""Return a sensible size."""
return QSize(8, 8)