base_crash_reporter.py (5160B)
1 # Electrum - lightweight Bitcoin client 2 # 3 # Permission is hereby granted, free of charge, to any person 4 # obtaining a copy of this software and associated documentation files 5 # (the "Software"), to deal in the Software without restriction, 6 # including without limitation the rights to use, copy, modify, merge, 7 # publish, distribute, sublicense, and/or sell copies of the Software, 8 # and to permit persons to whom the Software is furnished to do so, 9 # subject to the following conditions: 10 # 11 # The above copyright notice and this permission notice shall be 12 # included in all copies or substantial portions of the Software. 13 # 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 18 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 # SOFTWARE. 22 import asyncio 23 import json 24 import locale 25 import traceback 26 import sys 27 28 from .version import ELECTRUM_VERSION 29 from . import constants 30 from .i18n import _ 31 from .util import make_aiohttp_session 32 from .logging import describe_os_version, Logger, get_git_version 33 34 35 class BaseCrashReporter(Logger): 36 report_server = "https://crashhub.electrum.org" 37 config_key = "show_crash_reporter" 38 issue_template = """<h2>Traceback</h2> 39 <pre> 40 {traceback} 41 </pre> 42 43 <h2>Additional information</h2> 44 <ul> 45 <li>Electrum version: {app_version}</li> 46 <li>Python version: {python_version}</li> 47 <li>Operating system: {os}</li> 48 <li>Wallet type: {wallet_type}</li> 49 <li>Locale: {locale}</li> 50 </ul> 51 """ 52 CRASH_MESSAGE = _('Something went wrong while executing Electrum.') 53 CRASH_TITLE = _('Sorry!') 54 REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains ' 55 'useful debug information:') 56 DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):") 57 ASK_CONFIRM_SEND = _("Do you want to send this report?") 58 59 def __init__(self, exctype, value, tb): 60 Logger.__init__(self) 61 self.exc_args = (exctype, value, tb) 62 63 def send_report(self, asyncio_loop, proxy, endpoint="/crash", *, timeout=None): 64 if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server: 65 # Gah! Some kind of altcoin wants to send us crash reports. 66 raise Exception(_("Missing report URL.")) 67 report = self.get_traceback_info() 68 report.update(self.get_additional_info()) 69 report = json.dumps(report) 70 coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report) 71 response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout) 72 return response 73 74 async def do_post(self, proxy, url, data): 75 async with make_aiohttp_session(proxy) as session: 76 async with session.post(url, data=data, raise_for_status=True) as resp: 77 return await resp.text() 78 79 def get_traceback_info(self): 80 exc_string = str(self.exc_args[1]) 81 stack = traceback.extract_tb(self.exc_args[2]) 82 readable_trace = "".join(traceback.format_list(stack)) 83 id = { 84 "file": stack[-1].filename, 85 "name": stack[-1].name, 86 "type": self.exc_args[0].__name__ 87 } 88 return { 89 "exc_string": exc_string, 90 "stack": readable_trace, 91 "id": id 92 } 93 94 def get_additional_info(self): 95 args = { 96 "app_version": get_git_version() or ELECTRUM_VERSION, 97 "python_version": sys.version, 98 "os": describe_os_version(), 99 "wallet_type": "unknown", 100 "locale": locale.getdefaultlocale()[0] or "?", 101 "description": self.get_user_description() 102 } 103 try: 104 args["wallet_type"] = self.get_wallet_type() 105 except: 106 # Maybe the wallet isn't loaded yet 107 pass 108 return args 109 110 def _get_traceback_str(self) -> str: 111 return "".join(traceback.format_exception(*self.exc_args)) 112 113 def get_report_string(self): 114 info = self.get_additional_info() 115 info["traceback"] = self._get_traceback_str() 116 return self.issue_template.format(**info) 117 118 def get_user_description(self): 119 raise NotImplementedError 120 121 def get_wallet_type(self) -> str: 122 raise NotImplementedError 123 124 125 def trigger_crash(): 126 # note: do not change the type of the exception, the message, 127 # or the name of this method. All reports generated through this 128 # method will be grouped together by the crash reporter, and thus 129 # don't spam the issue tracker. 130 131 class TestingException(Exception): 132 pass 133 134 def crash_test(): 135 raise TestingException("triggered crash for testing purposes") 136 137 import threading 138 t = threading.Thread(target=crash_test) 139 t.start()