qml: initial crash handler impl
This commit is contained in:
@@ -15,14 +15,15 @@ try:
|
||||
except Exception:
|
||||
sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'")
|
||||
|
||||
from PyQt5.QtCore import QLocale, QTimer
|
||||
from PyQt5.QtCore import (Qt, QCoreApplication, QObject, QLocale, QTimer, pyqtSignal,
|
||||
QT_VERSION_STR, PYQT_VERSION_STR)
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
import PyQt5.QtCore as QtCore
|
||||
|
||||
from electrum.i18n import set_language, languages
|
||||
from electrum.plugin import run_hook
|
||||
from electrum.util import (profiler)
|
||||
from electrum.util import profiler
|
||||
from electrum.logging import Logger
|
||||
from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.daemon import Daemon
|
||||
@@ -40,19 +41,19 @@ class ElectrumGui(Logger):
|
||||
#os.environ['QML_IMPORT_TRACE'] = '1'
|
||||
#os.environ['QT_DEBUG_PLUGINS'] = '1'
|
||||
|
||||
self.logger.info(f"Qml GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}")
|
||||
self.logger.info(f"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}")
|
||||
self.logger.info("CWD=%s" % os.getcwd())
|
||||
# Uncomment this call to verify objects are being properly
|
||||
# GC-ed when windows are closed
|
||||
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
|
||||
# ElectrumWindow], interval=5)])
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
||||
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
||||
QCoreApplication.setAttribute(Qt.AA_X11InitThreads)
|
||||
if hasattr(Qt, "AA_ShareOpenGLContexts"):
|
||||
QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
|
||||
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
||||
QGuiApplication.setDesktopFileName('electrum.desktop')
|
||||
if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"):
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
||||
if hasattr(Qt, "AA_EnableHighDpiScaling"):
|
||||
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||
|
||||
if "QT_QUICK_CONTROLS_STYLE" not in os.environ:
|
||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||
@@ -60,14 +61,15 @@ class ElectrumGui(Logger):
|
||||
self.gui_thread = threading.current_thread()
|
||||
self.plugins = plugins
|
||||
self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins)
|
||||
|
||||
# timer
|
||||
self.timer = QTimer(self.app)
|
||||
self.timer.setSingleShot(False)
|
||||
self.timer.setInterval(500) # msec
|
||||
self.timer.timeout.connect(lambda: None) # periodically enter python scope
|
||||
|
||||
sys.excepthook = self.excepthook
|
||||
threading.excepthook = self.texcepthook
|
||||
# hook for crash reporter
|
||||
Exception_Hook.maybe_setup(config=config, slot=self.app.appController.crash)
|
||||
|
||||
# Initialize any QML plugins
|
||||
run_hook('init_qml', self)
|
||||
@@ -76,18 +78,6 @@ class ElectrumGui(Logger):
|
||||
def close(self):
|
||||
self.app.quit()
|
||||
|
||||
def excepthook(self, exc_type, exc_value, exc_tb):
|
||||
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
self.logger.exception(tb)
|
||||
self.app._valid = False
|
||||
self.close()
|
||||
|
||||
def texcepthook(self, arg):
|
||||
tb = "".join(traceback.format_exception(arg.exc_type, arg.exc_value, arg.exc_tb))
|
||||
self.logger.exception(tb)
|
||||
self.app._valid = False
|
||||
self.close()
|
||||
|
||||
def main(self):
|
||||
if not self.app._valid:
|
||||
return
|
||||
@@ -106,3 +96,36 @@ class ElectrumGui(Logger):
|
||||
name = QLocale.system().name()
|
||||
return name if name in languages else 'en_UK'
|
||||
|
||||
|
||||
|
||||
class Exception_Hook(QObject, Logger):
|
||||
_report_exception = pyqtSignal(object, object, object, object)
|
||||
|
||||
_INSTANCE = None # type: Optional[Exception_Hook] # singleton
|
||||
|
||||
def __init__(self, *, config: 'SimpleConfig', slot):
|
||||
QObject.__init__(self)
|
||||
Logger.__init__(self)
|
||||
assert self._INSTANCE is None, "Exception_Hook is supposed to be a singleton"
|
||||
self.config = config
|
||||
self.wallet_types_seen = set() # type: Set[str]
|
||||
|
||||
sys.excepthook = self.handler
|
||||
threading.excepthook = self.handler
|
||||
|
||||
self._report_exception.connect(slot)
|
||||
EarlyExceptionsQueue.set_hook_as_ready()
|
||||
|
||||
@classmethod
|
||||
def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None, slot = None) -> None:
|
||||
if not config.get(BaseCrashReporter.config_key, default=True):
|
||||
EarlyExceptionsQueue.set_hook_as_ready() # flush already queued exceptions
|
||||
return
|
||||
if not cls._INSTANCE:
|
||||
cls._INSTANCE = Exception_Hook(config=config, slot=slot)
|
||||
if wallet:
|
||||
cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type)
|
||||
|
||||
def handler(self, *exc_info):
|
||||
self.logger.error('exception caught by crash reporter', exc_info=exc_info)
|
||||
self._report_exception.emit(self.config, *exc_info)
|
||||
|
||||
157
electrum/gui/qml/components/ExceptionDialog.qml
Normal file
157
electrum/gui/qml/components/ExceptionDialog.qml
Normal file
@@ -0,0 +1,157 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Material 2.0
|
||||
|
||||
import QtQml 2.6
|
||||
|
||||
import "controls"
|
||||
|
||||
ElDialog
|
||||
{
|
||||
id: root
|
||||
|
||||
property var crashData
|
||||
|
||||
property bool _sending: false
|
||||
|
||||
modal: true
|
||||
parent: Overlay.overlay
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#aa000000"
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
header: null
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
enabled: !_sending
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: 128
|
||||
Layout.preferredHeight: 128
|
||||
source: '../../icons/bug.png'
|
||||
}
|
||||
Label {
|
||||
text: qsTr('Sorry!')
|
||||
font.pixelSize: constants.fontSizeLarge
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('Something went wrong while executing Electrum.')
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug information:')
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
text: qsTr('Show report contents')
|
||||
onClicked: {
|
||||
console.log('traceback: ' + crashData.traceback.stack)
|
||||
var dialog = report.createObject(app, {
|
||||
reportText: crashData.reportstring
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('Please briefly describe what led to the error (optional):')
|
||||
}
|
||||
TextArea {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
background: Rectangle {
|
||||
color: Qt.darker(Material.background, 1.25)
|
||||
}
|
||||
onTextChanged: AppController.setCrashUserText(text)
|
||||
}
|
||||
Label {
|
||||
text: qsTr('Do you want to send this report?')
|
||||
}
|
||||
RowLayout {
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 3
|
||||
text: qsTr('Send Bug Report')
|
||||
onClicked: AppController.sendReport()
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 2
|
||||
text: qsTr('Never')
|
||||
onClicked: {
|
||||
AppController.showNever()
|
||||
close()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 2
|
||||
text: qsTr('Not Now')
|
||||
onClicked: close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: _sending
|
||||
}
|
||||
|
||||
Component {
|
||||
id: report
|
||||
ElDialog {
|
||||
property string reportText
|
||||
|
||||
z: 3000
|
||||
modal: true
|
||||
parent: Overlay.overlay
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#aa000000"
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
header: null
|
||||
|
||||
Label {
|
||||
text: reportText
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AppController
|
||||
function onSendingBugreportSuccess(text) {
|
||||
_sending = false
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
text: text,
|
||||
richText: true
|
||||
})
|
||||
dialog.open()
|
||||
close()
|
||||
}
|
||||
function onSendingBugreportFailure(text) {
|
||||
_sending = false
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
text: text,
|
||||
richText: true
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
function onSendingBugreport() {
|
||||
_sending = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,11 @@ ApplicationWindow
|
||||
id: notificationPopup
|
||||
}
|
||||
|
||||
Component {
|
||||
id: crashDialog
|
||||
ExceptionDialog {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
coverTimer.start()
|
||||
|
||||
@@ -331,6 +336,12 @@ ApplicationWindow
|
||||
function onUserNotify(message) {
|
||||
notificationPopup.show(message)
|
||||
}
|
||||
function onShowException() {
|
||||
var dialog = crashDialog.createObject(app, {
|
||||
crashData: AppController.crashData()
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -2,14 +2,21 @@ import re
|
||||
import queue
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import html
|
||||
import threading
|
||||
import asyncio
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
|
||||
from PyQt5.QtGui import QGuiApplication, QFontDatabase
|
||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine
|
||||
|
||||
from electrum import version
|
||||
from electrum import version, constants
|
||||
from electrum.i18n import _
|
||||
from electrum.logging import Logger, get_logger
|
||||
from electrum.util import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME
|
||||
from electrum.base_crash_reporter import BaseCrashReporter
|
||||
from electrum.network import Network
|
||||
|
||||
from .qeconfig import QEConfig
|
||||
from .qedaemon import QEDaemon
|
||||
@@ -33,15 +40,20 @@ from .qewizard import QENewWalletWizard, QEServerConnectWizard
|
||||
|
||||
notification = None
|
||||
|
||||
class QEAppController(QObject):
|
||||
class QEAppController(BaseCrashReporter, QObject):
|
||||
_dummy = pyqtSignal()
|
||||
userNotify = pyqtSignal(str)
|
||||
uriReceived = pyqtSignal(str)
|
||||
showException = pyqtSignal()
|
||||
sendingBugreport = pyqtSignal()
|
||||
sendingBugreportSuccess = pyqtSignal(str)
|
||||
sendingBugreportFailure = pyqtSignal(str)
|
||||
|
||||
_dummy = pyqtSignal()
|
||||
_crash_user_text = ''
|
||||
|
||||
def __init__(self, qedaemon, plugins):
|
||||
super().__init__()
|
||||
self.logger = get_logger(__name__)
|
||||
BaseCrashReporter.__init__(self, None, None, None)
|
||||
QObject.__init__(self)
|
||||
|
||||
self._qedaemon = qedaemon
|
||||
self._plugins = plugins
|
||||
@@ -192,6 +204,60 @@ class QEAppController(QObject):
|
||||
def isAndroid(self):
|
||||
return 'ANDROID_DATA' in os.environ
|
||||
|
||||
@pyqtSlot(result='QVariantMap')
|
||||
def crashData(self):
|
||||
return {
|
||||
'traceback': self.get_traceback_info(),
|
||||
'extra': self.get_additional_info(),
|
||||
'reportstring': self.get_report_string()
|
||||
}
|
||||
|
||||
@pyqtSlot(object,object,object,object)
|
||||
def crash(self, config, e, text, tb):
|
||||
self.exc_args = (e, text, tb) # for BaseCrashReporter
|
||||
self.showException.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def sendReport(self):
|
||||
network = Network.get_instance()
|
||||
proxy = network.proxy
|
||||
|
||||
def report_task():
|
||||
try:
|
||||
response = BaseCrashReporter.send_report(self, network.asyncio_loop, proxy)
|
||||
self.sendingBugreportSuccess.emit(response)
|
||||
except Exception as e:
|
||||
self.logger.error('There was a problem with the automatic reporting', exc_info=e)
|
||||
self.sendingBugreportFailure.emit(_('There was a problem with the automatic reporting:') + '<br/>' +
|
||||
repr(e)[:120] + '<br/><br/>' +
|
||||
_("Please report this issue manually") +
|
||||
f' <a href="{constants.GIT_REPO_ISSUES_URL}">on GitHub</a>.')
|
||||
|
||||
self.sendingBugreport.emit()
|
||||
threading.Thread(target=report_task).start()
|
||||
|
||||
@pyqtSlot()
|
||||
def showNever(self):
|
||||
self.config.set_key(BaseCrashReporter.config_key, False)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setCrashUserText(self, text):
|
||||
self._crash_user_text = text
|
||||
|
||||
def _get_traceback_str_to_display(self) -> str:
|
||||
# The msg_box that shows the report uses rich_text=True, so
|
||||
# if traceback contains special HTML characters, e.g. '<',
|
||||
# they need to be escaped to avoid formatting issues.
|
||||
traceback_str = super()._get_traceback_str_to_display()
|
||||
return html.escape(traceback_str).replace(''',''')
|
||||
|
||||
def get_user_description(self):
|
||||
return self._crash_user_text
|
||||
|
||||
def get_wallet_type(self):
|
||||
wallet_types = Exception_Hook._INSTANCE.wallet_types_seen
|
||||
return ",".join(wallet_types)
|
||||
|
||||
class ElectrumQmlApplication(QGuiApplication):
|
||||
|
||||
_valid = True
|
||||
|
||||
Reference in New Issue
Block a user