exception_window.py (7363B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # 5 # Permission is hereby granted, free of charge, to any person 6 # obtaining a copy of this software and associated documentation files 7 # (the "Software"), to deal in the Software without restriction, 8 # including without limitation the rights to use, copy, modify, merge, 9 # publish, distribute, sublicense, and/or sell copies of the Software, 10 # and to permit persons to whom the Software is furnished to do so, 11 # subject to the following conditions: 12 # 13 # The above copyright notice and this permission notice shall be 14 # included in all copies or substantial portions of the Software. 15 # 16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 import sys 25 import html 26 from typing import TYPE_CHECKING, Optional, Set 27 28 from PyQt5.QtCore import QObject 29 import PyQt5.QtCore as QtCore 30 from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit, 31 QMessageBox, QHBoxLayout, QVBoxLayout) 32 33 from electrum.i18n import _ 34 from electrum.base_crash_reporter import BaseCrashReporter 35 from electrum.logging import Logger 36 from electrum import constants 37 from electrum.network import Network 38 39 from .util import MessageBoxMixin, read_QIcon, WaitingDialog 40 41 if TYPE_CHECKING: 42 from electrum.simple_config import SimpleConfig 43 from electrum.wallet import Abstract_Wallet 44 45 46 class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger): 47 _active_window = None 48 49 def __init__(self, config: 'SimpleConfig', exctype, value, tb): 50 BaseCrashReporter.__init__(self, exctype, value, tb) 51 self.network = Network.get_instance() 52 self.config = config 53 54 QWidget.__init__(self) 55 self.setWindowTitle('Electrum - ' + _('An Error Occurred')) 56 self.setMinimumSize(600, 300) 57 58 Logger.__init__(self) 59 60 main_box = QVBoxLayout() 61 62 heading = QLabel('<h2>' + BaseCrashReporter.CRASH_TITLE + '</h2>') 63 main_box.addWidget(heading) 64 main_box.addWidget(QLabel(BaseCrashReporter.CRASH_MESSAGE)) 65 66 main_box.addWidget(QLabel(BaseCrashReporter.REQUEST_HELP_MESSAGE)) 67 68 collapse_info = QPushButton(_("Show report contents")) 69 collapse_info.clicked.connect( 70 lambda: self.msg_box(QMessageBox.NoIcon, 71 self, _("Report contents"), self.get_report_string(), 72 rich_text=True)) 73 74 main_box.addWidget(collapse_info) 75 76 main_box.addWidget(QLabel(BaseCrashReporter.DESCRIBE_ERROR_MESSAGE)) 77 78 self.description_textfield = QTextEdit() 79 self.description_textfield.setFixedHeight(50) 80 self.description_textfield.setPlaceholderText(_("Do not enter sensitive/private information here. " 81 "The report will be visible on the public issue tracker.")) 82 main_box.addWidget(self.description_textfield) 83 84 main_box.addWidget(QLabel(BaseCrashReporter.ASK_CONFIRM_SEND)) 85 86 buttons = QHBoxLayout() 87 88 report_button = QPushButton(_('Send Bug Report')) 89 report_button.clicked.connect(self.send_report) 90 report_button.setIcon(read_QIcon("tab_send.png")) 91 buttons.addWidget(report_button) 92 93 never_button = QPushButton(_('Never')) 94 never_button.clicked.connect(self.show_never) 95 buttons.addWidget(never_button) 96 97 close_button = QPushButton(_('Not Now')) 98 close_button.clicked.connect(self.close) 99 buttons.addWidget(close_button) 100 101 main_box.addLayout(buttons) 102 103 self.setLayout(main_box) 104 self.show() 105 106 def send_report(self): 107 def on_success(response): 108 # note: 'response' coming from (remote) crash reporter server. 109 # It contains a URL to the GitHub issue, so we allow rich text. 110 self.show_message(parent=self, 111 title=_("Crash report"), 112 msg=response, 113 rich_text=True) 114 self.close() 115 def on_failure(exc_info): 116 e = exc_info[1] 117 self.logger.error('There was a problem with the automatic reporting', exc_info=exc_info) 118 self.show_critical(parent=self, 119 msg=(_('There was a problem with the automatic reporting:') + '<br/>' + 120 repr(e)[:120] + '<br/><br/>' + 121 _("Please report this issue manually") + 122 f' <a href="{constants.GIT_REPO_ISSUES_URL}">on GitHub</a>.'), 123 rich_text=True) 124 125 proxy = self.network.proxy 126 task = lambda: BaseCrashReporter.send_report(self, self.network.asyncio_loop, proxy) 127 msg = _('Sending crash report...') 128 WaitingDialog(self, msg, task, on_success, on_failure) 129 130 def on_close(self): 131 Exception_Window._active_window = None 132 self.close() 133 134 def show_never(self): 135 self.config.set_key(BaseCrashReporter.config_key, False) 136 self.close() 137 138 def closeEvent(self, event): 139 self.on_close() 140 event.accept() 141 142 def get_user_description(self): 143 return self.description_textfield.toPlainText() 144 145 def get_wallet_type(self): 146 wallet_types = Exception_Hook._INSTANCE.wallet_types_seen 147 return ",".join(wallet_types) 148 149 def _get_traceback_str(self) -> str: 150 # The msg_box that shows the report uses rich_text=True, so 151 # if traceback contains special HTML characters, e.g. '<', 152 # they need to be escaped to avoid formatting issues. 153 traceback_str = super()._get_traceback_str() 154 return html.escape(traceback_str) 155 156 157 def _show_window(*args): 158 if not Exception_Window._active_window: 159 Exception_Window._active_window = Exception_Window(*args) 160 161 162 class Exception_Hook(QObject, Logger): 163 _report_exception = QtCore.pyqtSignal(object, object, object, object) 164 165 _INSTANCE = None # type: Optional[Exception_Hook] # singleton 166 167 def __init__(self, *, config: 'SimpleConfig'): 168 QObject.__init__(self) 169 Logger.__init__(self) 170 assert self._INSTANCE is None, "Exception_Hook is supposed to be a singleton" 171 self.config = config 172 self.wallet_types_seen = set() # type: Set[str] 173 174 sys.excepthook = self.handler 175 self._report_exception.connect(_show_window) 176 177 @classmethod 178 def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet') -> None: 179 if not config.get(BaseCrashReporter.config_key, default=True): 180 return 181 if not cls._INSTANCE: 182 cls._INSTANCE = Exception_Hook(config=config) 183 cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type) 184 185 def handler(self, *exc_info): 186 self.logger.error('exception caught by crash reporter', exc_info=exc_info) 187 self._report_exception.emit(self.config, *exc_info)