crash_reporter.py (6768B)
1 import sys 2 import json 3 4 from aiohttp.client_exceptions import ClientError 5 from kivy import base, utils 6 from kivy.clock import Clock 7 from kivy.core.window import Window 8 from kivy.factory import Factory 9 from kivy.lang import Builder 10 from kivy.uix.label import Label 11 from kivy.utils import platform 12 13 from electrum.gui.kivy.i18n import _ 14 15 from electrum.base_crash_reporter import BaseCrashReporter 16 from electrum.logging import Logger 17 18 19 Builder.load_string(''' 20 <CrashReporter@Popup> 21 BoxLayout: 22 orientation: 'vertical' 23 Label: 24 id: crash_message 25 text_size: root.width, None 26 size: self.texture_size 27 size_hint: None, None 28 Label: 29 id: request_help_message 30 text_size: root.width*.95, None 31 size: self.texture_size 32 size_hint: None, None 33 BoxLayout: 34 size_hint: 1, 0.1 35 Button: 36 text: 'Show report contents' 37 height: '48dp' 38 size_hint: 1, None 39 on_release: root.show_contents() 40 BoxLayout: 41 size_hint: 1, 0.1 42 Label: 43 id: describe_error_message 44 text_size: root.width, None 45 size: self.texture_size 46 size_hint: None, None 47 TextInput: 48 id: user_message 49 size_hint: 1, 0.3 50 BoxLayout: 51 size_hint: 1, 0.7 52 BoxLayout: 53 size_hint: 1, None 54 height: '48dp' 55 orientation: 'horizontal' 56 Button: 57 height: '48dp' 58 text: 'Send' 59 on_release: root.send_report() 60 Button: 61 text: 'Never' 62 on_release: root.show_never() 63 Button: 64 text: 'Not now' 65 on_release: root.dismiss() 66 67 <CrashReportDetails@Popup> 68 BoxLayout: 69 orientation: 'vertical' 70 ScrollView: 71 do_scroll_x: False 72 Label: 73 id: contents 74 text_size: root.width*.9, None 75 size: self.texture_size 76 size_hint: None, None 77 Button: 78 text: 'Close' 79 height: '48dp' 80 size_hint: 1, None 81 on_release: root.dismiss() 82 ''') 83 84 85 class CrashReporter(BaseCrashReporter, Factory.Popup): 86 issue_template = """[b]Traceback[/b] 87 88 [i]{traceback}[/i] 89 90 91 [b]Additional information[/b] 92 * Electrum version: {app_version} 93 * Operating system: {os} 94 * Wallet type: {wallet_type} 95 * Locale: {locale} 96 """ 97 98 def __init__(self, main_window, exctype, value, tb): 99 BaseCrashReporter.__init__(self, exctype, value, tb) 100 Factory.Popup.__init__(self) 101 self.main_window = main_window 102 self.title = BaseCrashReporter.CRASH_TITLE 103 self.title_size = "24sp" 104 self.ids.crash_message.text = BaseCrashReporter.CRASH_MESSAGE 105 self.ids.request_help_message.text = BaseCrashReporter.REQUEST_HELP_MESSAGE 106 self.ids.describe_error_message.text = BaseCrashReporter.DESCRIBE_ERROR_MESSAGE 107 108 def show_contents(self): 109 details = CrashReportDetails(self.get_report_string()) 110 details.open() 111 112 def show_popup(self, title, content): 113 popup = Factory.Popup(title=title, 114 content=Label(text=content, text_size=(Window.size[0] * 3/4, None)), 115 size_hint=(3/4, 3/4)) 116 popup.open() 117 118 def send_report(self): 119 try: 120 loop = self.main_window.network.asyncio_loop 121 proxy = self.main_window.network.proxy 122 # FIXME network request in GUI thread... 123 response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, 124 "/crash.json", timeout=10)) 125 except (ValueError, ClientError) as e: 126 self.logger.warning(f"Error sending crash report. exc={e!r}") 127 self.show_popup(_('Unable to send report'), _("Please check your network connection.")) 128 else: 129 self.show_popup(_('Report sent'), response["text"]) 130 location = response["location"] 131 if location: 132 self.logger.info(f"Crash report sent. location={location!r}") 133 self.open_url(location) 134 self.dismiss() 135 136 def on_dismiss(self): 137 self.main_window.on_wizard_aborted() 138 139 def open_url(self, url): 140 if platform != 'android': 141 return 142 from jnius import autoclass, cast 143 String = autoclass("java.lang.String") 144 url = String(url) 145 PythonActivity = autoclass('org.kivy.android.PythonActivity') 146 activity = PythonActivity.mActivity 147 Intent = autoclass('android.content.Intent') 148 Uri = autoclass('android.net.Uri') 149 browserIntent = Intent() 150 # This line crashes the app: 151 # browserIntent.setAction(Intent.ACTION_VIEW) 152 # Luckily we don't need it because the OS is smart enough to recognize the URL 153 browserIntent.setData(Uri.parse(url)) 154 currentActivity = cast('android.app.Activity', activity) 155 currentActivity.startActivity(browserIntent) 156 157 def show_never(self): 158 self.main_window.electrum_config.set_key(BaseCrashReporter.config_key, False) 159 self.dismiss() 160 161 def get_user_description(self): 162 return self.ids.user_message.text 163 164 def get_wallet_type(self): 165 return self.main_window.wallet.wallet_type 166 167 168 class CrashReportDetails(Factory.Popup): 169 def __init__(self, text): 170 Factory.Popup.__init__(self) 171 self.title = "Report Details" 172 self.ids.contents.text = text 173 print(text) 174 175 176 class ExceptionHook(base.ExceptionHandler, Logger): 177 def __init__(self, main_window): 178 base.ExceptionHandler.__init__(self) 179 Logger.__init__(self) 180 self.main_window = main_window 181 if not main_window.electrum_config.get(BaseCrashReporter.config_key, default=True): 182 return 183 # For exceptions in Kivy: 184 base.ExceptionManager.add_handler(self) 185 # For everything else: 186 sys.excepthook = lambda exctype, value, tb: self.handle_exception(value) 187 188 def handle_exception(self, _inst): 189 exc_info = sys.exc_info() 190 self.logger.error('exception caught by crash reporter', exc_info=exc_info) 191 # Check if this is an exception from within the exception handler: 192 import traceback 193 for item in traceback.extract_tb(exc_info[2]): 194 if item.filename.endswith("crash_reporter.py"): 195 return 196 e = CrashReporter(self.main_window, *exc_info) 197 # Open in main thread: 198 Clock.schedule_once(lambda _: e.open(), 0) 199 return base.ExceptionManager.PASS