qml: initial crash handler impl
This commit is contained in:
@@ -15,14 +15,15 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'")
|
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
|
from PyQt5.QtGui import QGuiApplication
|
||||||
import PyQt5.QtCore as QtCore
|
|
||||||
|
|
||||||
from electrum.i18n import set_language, languages
|
from electrum.i18n import set_language, languages
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
from electrum.util import (profiler)
|
from electrum.util import profiler
|
||||||
from electrum.logging import Logger
|
from electrum.logging import Logger
|
||||||
|
from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from electrum.daemon import Daemon
|
from electrum.daemon import Daemon
|
||||||
@@ -40,19 +41,19 @@ class ElectrumGui(Logger):
|
|||||||
#os.environ['QML_IMPORT_TRACE'] = '1'
|
#os.environ['QML_IMPORT_TRACE'] = '1'
|
||||||
#os.environ['QT_DEBUG_PLUGINS'] = '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())
|
self.logger.info("CWD=%s" % os.getcwd())
|
||||||
# Uncomment this call to verify objects are being properly
|
# Uncomment this call to verify objects are being properly
|
||||||
# GC-ed when windows are closed
|
# GC-ed when windows are closed
|
||||||
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
|
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
|
||||||
# ElectrumWindow], interval=5)])
|
# ElectrumWindow], interval=5)])
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
QCoreApplication.setAttribute(Qt.AA_X11InitThreads)
|
||||||
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
if hasattr(Qt, "AA_ShareOpenGLContexts"):
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
|
||||||
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
||||||
QGuiApplication.setDesktopFileName('electrum.desktop')
|
QGuiApplication.setDesktopFileName('electrum.desktop')
|
||||||
if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"):
|
if hasattr(Qt, "AA_EnableHighDpiScaling"):
|
||||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
|
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
|
|
||||||
if "QT_QUICK_CONTROLS_STYLE" not in os.environ:
|
if "QT_QUICK_CONTROLS_STYLE" not in os.environ:
|
||||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||||
@@ -60,14 +61,15 @@ class ElectrumGui(Logger):
|
|||||||
self.gui_thread = threading.current_thread()
|
self.gui_thread = threading.current_thread()
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins)
|
self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins)
|
||||||
|
|
||||||
# timer
|
# timer
|
||||||
self.timer = QTimer(self.app)
|
self.timer = QTimer(self.app)
|
||||||
self.timer.setSingleShot(False)
|
self.timer.setSingleShot(False)
|
||||||
self.timer.setInterval(500) # msec
|
self.timer.setInterval(500) # msec
|
||||||
self.timer.timeout.connect(lambda: None) # periodically enter python scope
|
self.timer.timeout.connect(lambda: None) # periodically enter python scope
|
||||||
|
|
||||||
sys.excepthook = self.excepthook
|
# hook for crash reporter
|
||||||
threading.excepthook = self.texcepthook
|
Exception_Hook.maybe_setup(config=config, slot=self.app.appController.crash)
|
||||||
|
|
||||||
# Initialize any QML plugins
|
# Initialize any QML plugins
|
||||||
run_hook('init_qml', self)
|
run_hook('init_qml', self)
|
||||||
@@ -76,18 +78,6 @@ class ElectrumGui(Logger):
|
|||||||
def close(self):
|
def close(self):
|
||||||
self.app.quit()
|
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):
|
def main(self):
|
||||||
if not self.app._valid:
|
if not self.app._valid:
|
||||||
return
|
return
|
||||||
@@ -106,3 +96,36 @@ class ElectrumGui(Logger):
|
|||||||
name = QLocale.system().name()
|
name = QLocale.system().name()
|
||||||
return name if name in languages else 'en_UK'
|
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
|
id: notificationPopup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: crashDialog
|
||||||
|
ExceptionDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
coverTimer.start()
|
coverTimer.start()
|
||||||
|
|
||||||
@@ -331,6 +336,12 @@ ApplicationWindow
|
|||||||
function onUserNotify(message) {
|
function onUserNotify(message) {
|
||||||
notificationPopup.show(message)
|
notificationPopup.show(message)
|
||||||
}
|
}
|
||||||
|
function onShowException() {
|
||||||
|
var dialog = crashDialog.createObject(app, {
|
||||||
|
crashData: AppController.crashData()
|
||||||
|
})
|
||||||
|
dialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ import re
|
|||||||
import queue
|
import queue
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import html
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
|
||||||
from PyQt5.QtGui import QGuiApplication, QFontDatabase
|
from PyQt5.QtGui import QGuiApplication, QFontDatabase
|
||||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine
|
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.logging import Logger, get_logger
|
||||||
from electrum.util import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME
|
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 .qeconfig import QEConfig
|
||||||
from .qedaemon import QEDaemon
|
from .qedaemon import QEDaemon
|
||||||
@@ -33,15 +40,20 @@ from .qewizard import QENewWalletWizard, QEServerConnectWizard
|
|||||||
|
|
||||||
notification = None
|
notification = None
|
||||||
|
|
||||||
class QEAppController(QObject):
|
class QEAppController(BaseCrashReporter, QObject):
|
||||||
|
_dummy = pyqtSignal()
|
||||||
userNotify = pyqtSignal(str)
|
userNotify = pyqtSignal(str)
|
||||||
uriReceived = 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):
|
def __init__(self, qedaemon, plugins):
|
||||||
super().__init__()
|
BaseCrashReporter.__init__(self, None, None, None)
|
||||||
self.logger = get_logger(__name__)
|
QObject.__init__(self)
|
||||||
|
|
||||||
self._qedaemon = qedaemon
|
self._qedaemon = qedaemon
|
||||||
self._plugins = plugins
|
self._plugins = plugins
|
||||||
@@ -192,6 +204,60 @@ class QEAppController(QObject):
|
|||||||
def isAndroid(self):
|
def isAndroid(self):
|
||||||
return 'ANDROID_DATA' in os.environ
|
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):
|
class ElectrumQmlApplication(QGuiApplication):
|
||||||
|
|
||||||
_valid = True
|
_valid = True
|
||||||
|
|||||||
Reference in New Issue
Block a user