-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
canvas: add remote error reporting on unhandled exception
- Loading branch information
Showing
3 changed files
with
221 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import os | ||
import sys | ||
import time | ||
import logging | ||
import platform | ||
import traceback | ||
import uuid | ||
from html import escape | ||
from threading import Thread | ||
|
||
from tempfile import mkstemp | ||
from collections import OrderedDict | ||
from urllib.parse import urljoin, urlencode | ||
from urllib.request import pathname2url, urlopen | ||
from unittest.mock import patch | ||
|
||
from PyQt4.QtCore import pyqtSlot, QSettings, Qt | ||
from PyQt4.QtGui import ( | ||
QApplication, QCheckBox, QDesktopServices, QDialog, QFont, QHBoxLayout, | ||
QLabel, QMessageBox, QPushButton, QStyle, QTextBrowser, | ||
QVBoxLayout, QWidget | ||
) | ||
|
||
try: | ||
from Orange.widgets.widget import OWWidget | ||
from Orange.version import full_version as VERSION_STR | ||
except ImportError: | ||
# OWWidget (etc.) is not available because this is not Orange | ||
class OWWidget: pass | ||
VERSION_STR = '???' | ||
|
||
|
||
REPORT_POST_URL = 'http://qa.orange.biolab.si/error-report/v1/' | ||
|
||
log = logging.getLogger() | ||
|
||
|
||
class ErrorReporting(QDialog): | ||
_cache = set() # For errors already handled during one session | ||
|
||
class DataField: | ||
EXCEPTION = 'Exception' | ||
MODULE = 'Module' | ||
WIDGET_NAME = 'Widget Name' | ||
WIDGET_MODULE = 'Widget Module' | ||
VERSION = 'Version' | ||
ENVIRONMENT = 'Environment' | ||
MACHINE_ID = 'Machine ID' | ||
WIDGET_SCHEME = 'Widget Scheme' | ||
STACK_TRACE = 'Stack Trace' | ||
|
||
def __init__(self, data): | ||
icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning) | ||
F = self.DataField | ||
|
||
def _finished(*, key=(data[F.MODULE], data[F.WIDGET_MODULE]), | ||
filename=data[F.WIDGET_SCHEME]): | ||
self._cache.add(key) | ||
try: os.remove(filename) | ||
except Exception: pass | ||
|
||
super().__init__(None, Qt.Window, modal=True, | ||
sizeGripEnabled=True, windowIcon=icon, | ||
windowTitle='Unexpected Error', | ||
finished=_finished) | ||
self._data = data | ||
|
||
layout = QVBoxLayout(self) | ||
self.setLayout(layout) | ||
labels = QWidget(self) | ||
labels_layout = QHBoxLayout(self) | ||
labels.setLayout(labels_layout) | ||
labels_layout.addWidget(QLabel(pixmap=icon.pixmap(50, 50))) | ||
labels_layout.addWidget(QLabel( | ||
'The program encountered an unexpected error. Please<br>' | ||
'report it anonymously to the developers.<br><br>' | ||
'The following data will be reported:')) | ||
labels_layout.addStretch(1) | ||
layout.addWidget(labels) | ||
font = QFont('Monospace', 10) | ||
font.setStyleHint(QFont.Monospace) | ||
font.setFixedPitch(True) | ||
textbrowser = QTextBrowser(self, | ||
font=font, | ||
openLinks=False, | ||
lineWrapMode=QTextBrowser.NoWrap, | ||
anchorClicked=QDesktopServices.openUrl) | ||
layout.addWidget(textbrowser) | ||
|
||
def _reload_text(): | ||
add_scheme = cb.isChecked() | ||
settings.setValue('error-reporting/add-scheme', add_scheme) | ||
lines = ['<table>'] | ||
for k, v in data.items(): | ||
if k.startswith('_'): | ||
continue | ||
_v, v = v, escape(v) | ||
if k == F.WIDGET_SCHEME: | ||
if not add_scheme: | ||
continue | ||
v = '<a href="{}">{}</a>'.format(urljoin('file:', pathname2url(_v)), v) | ||
if k == F.STACK_TRACE: | ||
v = v.replace('\n', '<br>').replace(' ', ' ') | ||
lines.append('<tr><th align="left">{}:</th><td>{}</td></tr>'.format(k, v)) | ||
lines.append('</table>') | ||
textbrowser.setHtml(''.join(lines)) | ||
|
||
settings = QSettings() | ||
cb = QCheckBox( | ||
'Include workflow (data will NOT be transmitted)', self, | ||
checked=settings.value('error-reporting/add-scheme', True, type=bool)) | ||
cb.stateChanged.connect(_reload_text) | ||
_reload_text() | ||
|
||
layout.addWidget(cb) | ||
buttons = QWidget(self) | ||
buttons_layout = QHBoxLayout(self) | ||
buttons.setLayout(buttons_layout) | ||
buttons_layout.addWidget(QPushButton('Send Report (Thanks!)', default=True, clicked=self.accept)) | ||
buttons_layout.addWidget(QPushButton("Don't Send", default=False, clicked=self.reject)) | ||
layout.addWidget(buttons) | ||
|
||
def accept(self): | ||
super().accept() | ||
F = self.DataField | ||
data = self._data.copy() | ||
|
||
if QSettings().value('error-reporting/add-scheme', True, type=bool): | ||
data[F.WIDGET_SCHEME] = data['_' + F.WIDGET_SCHEME] | ||
del data['_' + F.WIDGET_SCHEME] | ||
|
||
def _post_report(data): | ||
MAX_RETRIES = 2 | ||
for _retry in range(MAX_RETRIES): | ||
try: | ||
urlopen(REPORT_POST_URL, | ||
timeout=10, | ||
data=urlencode(data).encode('utf8')) | ||
except Exception as e: | ||
if _retry == MAX_RETRIES - 1: | ||
e.__context__ = None | ||
log.exception('Error reporting failed', exc_info=e) | ||
time.sleep(10) | ||
continue | ||
break | ||
|
||
Thread(target=_post_report, args=(data,)).start() | ||
|
||
@classmethod | ||
@patch('sys.excepthook', sys.__excepthook__) # Prevent recursion | ||
@pyqtSlot(object) | ||
def handle_exception(cls, exc): | ||
(etype, evalue, tb), canvas = exc | ||
exception = traceback.format_exception_only(etype, evalue)[-1].strip() | ||
stacktrace = ''.join(traceback.format_exception(etype, evalue, tb)) | ||
|
||
def _find_last_frame(tb): | ||
while tb.tb_next: | ||
tb = tb.tb_next | ||
return tb | ||
|
||
frame = _find_last_frame(tb) | ||
err_module = '{}:{}'.format( | ||
frame.tb_frame.f_globals.get('__name__', frame.tb_frame.f_code.co_filename), | ||
frame.tb_lineno) | ||
|
||
def _find_widget_frame(tb): | ||
while tb: | ||
if isinstance(tb.tb_frame.f_locals.get('self'), OWWidget): | ||
return tb | ||
tb = tb.tb_next | ||
|
||
widget, frame = None, _find_widget_frame(tb) | ||
if frame: | ||
widget = frame.tb_frame.f_locals['self'].__class__ | ||
widget_module = '{}:{}'.format(widget.__module__, frame.tb_lineno) | ||
|
||
# If this exact error was already reported in this session, | ||
# just warn about it | ||
if (err_module, widget_module) in cls._cache: | ||
QMessageBox(QMessageBox.Warning, 'Error Encountered', | ||
'Error encountered{}:<br><br><tt>{}</tt>'.format( | ||
' in widget <b>{}</b>'.format(widget.name) if widget else '', | ||
stacktrace.replace('\n', '<br>').replace(' ', ' ')), | ||
QMessageBox.Ignore).exec() | ||
return | ||
|
||
F = cls.DataField | ||
data = OrderedDict() | ||
data[F.EXCEPTION] = exception | ||
data[F.MODULE] = err_module | ||
if widget: | ||
data[F.WIDGET_NAME] = widget.name | ||
data[F.WIDGET_MODULE] = widget_module | ||
if canvas: | ||
filename = mkstemp(prefix='ows-', suffix='.ows.xml')[1] | ||
# Prevent excepthook printing the same exception when | ||
# canvas tries to instantiate the broken widget again | ||
with patch('sys.excepthook', lambda *_: None): | ||
canvas.save_scheme_to(canvas.current_document().scheme(), filename) | ||
data[F.WIDGET_SCHEME] = filename | ||
with open(filename) as f: | ||
data['_' + F.WIDGET_SCHEME] = f.read() | ||
data[F.VERSION] = VERSION_STR | ||
data[F.ENVIRONMENT] = 'Python {} on {} {} {} {}'.format( | ||
platform.python_version(), platform.system(), platform.release(), | ||
platform.version(), platform.machine()) | ||
data[F.MACHINE_ID] = str(uuid.getnode()) | ||
data[F.STACK_TRACE] = stacktrace | ||
|
||
cls(data=data).exec() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters