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.
22import asyncio
23import json
24import locale
25import traceback
26import sys
27
28from .version import ELECTRUM_VERSION
29from . import constants
30from .i18n import _
31from .util import make_aiohttp_session
32from .logging import describe_os_version, Logger, get_git_version
33
34
35class 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    USER_COMMENT_PLACEHOLDER = _("Do not enter sensitive/private information here. "
59                                 "The report will be visible on the public issue tracker.")
60
61    def __init__(self, exctype, value, tb):
62        Logger.__init__(self)
63        self.exc_args = (exctype, value, tb)
64
65    def send_report(self, asyncio_loop, proxy, endpoint="/crash", *, timeout=None):
66        if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
67            # Gah! Some kind of altcoin wants to send us crash reports.
68            raise Exception(_("Missing report URL."))
69        report = self.get_traceback_info()
70        report.update(self.get_additional_info())
71        report = json.dumps(report)
72        coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report)
73        response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout)
74        return response
75
76    async def do_post(self, proxy, url, data):
77        async with make_aiohttp_session(proxy) as session:
78            async with session.post(url, data=data, raise_for_status=True) as resp:
79                return await resp.text()
80
81    def get_traceback_info(self):
82        exc_string = str(self.exc_args[1])
83        stack = traceback.extract_tb(self.exc_args[2])
84        readable_trace = self.__get_traceback_str_to_send()
85        id = {
86            "file": stack[-1].filename,
87            "name": stack[-1].name,
88            "type": self.exc_args[0].__name__
89        }
90        return {
91            "exc_string": exc_string,
92            "stack": readable_trace,
93            "id": id
94        }
95
96    def get_additional_info(self):
97        args = {
98            "app_version": get_git_version() or ELECTRUM_VERSION,
99            "python_version": sys.version,
100            "os": describe_os_version(),
101            "wallet_type": "unknown",
102            "locale": locale.getdefaultlocale()[0] or "?",
103            "description": self.get_user_description()
104        }
105        try:
106            args["wallet_type"] = self.get_wallet_type()
107        except:
108            # Maybe the wallet isn't loaded yet
109            pass
110        return args
111
112    def __get_traceback_str_to_send(self) -> str:
113        # make sure that traceback sent to crash reporter contains
114        # e.__context__ and e.__cause__, i.e. if there was a chain of
115        # exceptions, we want the full traceback for the whole chain.
116        return "".join(traceback.format_exception(*self.exc_args))
117
118    def _get_traceback_str_to_display(self) -> str:
119        # overridden in Qt subclass
120        return self.__get_traceback_str_to_send()
121
122    def get_report_string(self):
123        info = self.get_additional_info()
124        info["traceback"] = self._get_traceback_str_to_display()
125        return self.issue_template.format(**info)
126
127    def get_user_description(self):
128        raise NotImplementedError
129
130    def get_wallet_type(self) -> str:
131        raise NotImplementedError
132
133
134def trigger_crash():
135    # note: do not change the type of the exception, the message,
136    # or the name of this method. All reports generated through this
137    # method will be grouped together by the crash reporter, and thus
138    # don't spam the issue tracker.
139
140    class TestingException(Exception):
141        pass
142
143    def crash_test():
144        raise TestingException("triggered crash for testing purposes")
145
146    import threading
147    t = threading.Thread(target=crash_test)
148    t.start()
149