2018-06-12 14:17:34 +02:00
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
|
#
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person
|
|
|
|
|
# obtaining a copy of this software and associated documentation files
|
|
|
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
|
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
|
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
|
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
|
|
|
# subject to the following conditions:
|
|
|
|
|
#
|
|
|
|
|
# The above copyright notice and this permission notice shall be
|
|
|
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
|
#
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
|
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
|
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
# SOFTWARE.
|
2018-09-06 16:18:45 +02:00
|
|
|
import asyncio
|
2018-06-12 14:17:34 +02:00
|
|
|
import json
|
|
|
|
|
import locale
|
|
|
|
|
import traceback
|
|
|
|
|
import sys
|
2021-11-05 19:55:22 +01:00
|
|
|
import queue
|
2025-07-18 16:19:54 +00:00
|
|
|
from typing import TYPE_CHECKING, NamedTuple, Optional, TypedDict
|
|
|
|
|
from types import TracebackType
|
2018-06-12 14:17:34 +02:00
|
|
|
|
2018-07-11 17:38:47 +02:00
|
|
|
from .version import ELECTRUM_VERSION
|
2018-10-11 16:30:30 +02:00
|
|
|
from . import constants
|
2018-07-11 17:38:47 +02:00
|
|
|
from .i18n import _
|
2023-04-06 13:53:40 +00:00
|
|
|
from .util import make_aiohttp_session, error_text_str_to_safe_str
|
2021-03-10 16:23:49 +01:00
|
|
|
from .logging import describe_os_version, Logger, get_git_version
|
2025-07-18 16:34:54 +00:00
|
|
|
from .crypto import sha256
|
2018-06-12 14:17:34 +02:00
|
|
|
|
2025-03-03 13:34:05 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from .network import ProxySettings
|
|
|
|
|
|
2018-10-11 16:30:30 +02:00
|
|
|
|
2025-09-11 15:33:53 +00:00
|
|
|
_tainted_by_console = False
|
|
|
|
|
def taint_reports_by_console_usage():
|
|
|
|
|
global _tainted_by_console
|
|
|
|
|
_tainted_by_console = True
|
|
|
|
|
|
|
|
|
|
|
2023-03-16 19:07:33 +00:00
|
|
|
class CrashReportResponse(NamedTuple):
|
|
|
|
|
status: Optional[str]
|
|
|
|
|
text: str
|
|
|
|
|
url: Optional[str]
|
|
|
|
|
|
|
|
|
|
|
2019-07-02 19:27:36 +02:00
|
|
|
class BaseCrashReporter(Logger):
|
2018-06-12 14:17:34 +02:00
|
|
|
report_server = "https://crashhub.electrum.org"
|
|
|
|
|
issue_template = """<h2>Traceback</h2>
|
|
|
|
|
<pre>
|
|
|
|
|
{traceback}
|
|
|
|
|
</pre>
|
|
|
|
|
|
|
|
|
|
<h2>Additional information</h2>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>Electrum version: {app_version}</li>
|
2018-07-02 13:41:09 +02:00
|
|
|
<li>Python version: {python_version}</li>
|
2018-06-12 14:17:34 +02:00
|
|
|
<li>Operating system: {os}</li>
|
|
|
|
|
<li>Wallet type: {wallet_type}</li>
|
|
|
|
|
<li>Locale: {locale}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
CRASH_MESSAGE = _('Something went wrong while executing Electrum.')
|
|
|
|
|
CRASH_TITLE = _('Sorry!')
|
|
|
|
|
REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains '
|
|
|
|
|
'useful debug information:')
|
|
|
|
|
DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):")
|
|
|
|
|
ASK_CONFIRM_SEND = _("Do you want to send this report?")
|
2021-07-05 18:59:02 +02:00
|
|
|
USER_COMMENT_PLACEHOLDER = _("Do not enter sensitive/private information here. "
|
|
|
|
|
"The report will be visible on the public issue tracker.")
|
2018-06-12 14:17:34 +02:00
|
|
|
|
2025-07-18 16:19:54 +00:00
|
|
|
exc_args: tuple[type[BaseException], BaseException, TracebackType | None]
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
exctype: type[BaseException],
|
|
|
|
|
excvalue: BaseException,
|
|
|
|
|
tb: TracebackType | None,
|
|
|
|
|
):
|
2019-07-02 19:27:36 +02:00
|
|
|
Logger.__init__(self)
|
2025-07-18 16:19:54 +00:00
|
|
|
self.exc_args = (exctype, excvalue, tb)
|
2018-06-12 14:17:34 +02:00
|
|
|
|
2025-03-03 13:34:05 +01:00
|
|
|
def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse:
|
2023-03-16 19:07:33 +00:00
|
|
|
# FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API...
|
2025-06-11 14:14:49 +02:00
|
|
|
if (constants.net.GENESIS[-4:] not in [
|
|
|
|
|
"e26f", # mainnet
|
|
|
|
|
"4943", # testnet 3
|
|
|
|
|
] and ".electrum.org" in BaseCrashReporter.report_server):
|
2018-06-12 14:17:34 +02:00
|
|
|
# Gah! Some kind of altcoin wants to send us crash reports.
|
2018-06-20 18:13:43 +02:00
|
|
|
raise Exception(_("Missing report URL."))
|
2025-07-18 16:19:54 +00:00
|
|
|
report = self.get_traceback_info(*self.exc_args)
|
2018-06-12 14:17:34 +02:00
|
|
|
report.update(self.get_additional_info())
|
|
|
|
|
report = json.dumps(report)
|
2023-03-16 19:07:33 +00:00
|
|
|
coro = self.do_post(proxy, BaseCrashReporter.report_server + "/crash.json", data=report)
|
2019-07-02 19:27:36 +02:00
|
|
|
response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout)
|
2023-04-06 13:53:40 +00:00
|
|
|
self.logger.info(
|
|
|
|
|
f"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(response)}")
|
2023-03-16 19:07:33 +00:00
|
|
|
response = json.loads(response)
|
|
|
|
|
assert isinstance(response, dict), type(response)
|
|
|
|
|
# sanitize URL
|
|
|
|
|
if location := response.get("location"):
|
|
|
|
|
assert isinstance(location, str)
|
|
|
|
|
base_issues_url = constants.GIT_REPO_ISSUES_URL
|
|
|
|
|
if not base_issues_url.endswith("/"):
|
|
|
|
|
base_issues_url = base_issues_url + "/"
|
|
|
|
|
if not location.startswith(base_issues_url):
|
|
|
|
|
location = None
|
|
|
|
|
ret = CrashReportResponse(
|
|
|
|
|
status=response.get("status"),
|
|
|
|
|
url=location,
|
|
|
|
|
text=_("Thanks for reporting this issue!"),
|
|
|
|
|
)
|
|
|
|
|
return ret
|
2018-06-12 14:17:34 +02:00
|
|
|
|
2025-03-03 13:34:05 +01:00
|
|
|
async def do_post(self, proxy: 'ProxySettings', url, data) -> str:
|
2018-09-06 16:18:45 +02:00
|
|
|
async with make_aiohttp_session(proxy) as session:
|
2020-06-14 03:41:45 +02:00
|
|
|
async with session.post(url, data=data, raise_for_status=True) as resp:
|
2018-09-06 16:18:45 +02:00
|
|
|
return await resp.text()
|
|
|
|
|
|
2025-07-18 16:19:54 +00:00
|
|
|
@classmethod
|
|
|
|
|
def get_traceback_info(
|
|
|
|
|
cls,
|
|
|
|
|
exctype: type[BaseException],
|
|
|
|
|
excvalue: BaseException,
|
|
|
|
|
tb: TracebackType | None,
|
|
|
|
|
) -> TypedDict('TBInfo', {'exc_string': str, 'stack': str, 'id': dict[str, str]}):
|
|
|
|
|
exc_string = str(excvalue)
|
|
|
|
|
stack = traceback.extract_tb(tb)
|
|
|
|
|
readable_trace = cls._get_traceback_str_to_send(exctype, excvalue, tb)
|
|
|
|
|
_id = {
|
2023-02-01 10:42:49 +01:00
|
|
|
"file": stack[-1].filename if len(stack) else '<no stack>',
|
|
|
|
|
"name": stack[-1].name if len(stack) else '<no stack>',
|
2025-07-18 16:19:54 +00:00
|
|
|
"type": exctype.__name__
|
|
|
|
|
} # note: this is the "id" the crash reporter server uses to group together reports.
|
2018-06-12 14:17:34 +02:00
|
|
|
return {
|
|
|
|
|
"exc_string": exc_string,
|
|
|
|
|
"stack": readable_trace,
|
2025-07-18 16:19:54 +00:00
|
|
|
"id": _id,
|
2018-06-12 14:17:34 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-18 16:34:54 +00:00
|
|
|
@classmethod
|
|
|
|
|
def get_traceback_groupid_hash(
|
|
|
|
|
cls,
|
|
|
|
|
exctype: type[BaseException],
|
|
|
|
|
excvalue: BaseException,
|
|
|
|
|
tb: TracebackType | None,
|
|
|
|
|
) -> bytes:
|
|
|
|
|
tb_info = cls.get_traceback_info(exctype, excvalue, tb)
|
|
|
|
|
_id = tb_info["id"]
|
|
|
|
|
return sha256(str(_id))
|
|
|
|
|
|
2018-06-12 14:17:34 +02:00
|
|
|
def get_additional_info(self):
|
2025-09-11 15:33:53 +00:00
|
|
|
app_version = (get_git_version() or ELECTRUM_VERSION)
|
|
|
|
|
if _tainted_by_console:
|
|
|
|
|
app_version += "-consoletaint"
|
2018-06-12 14:17:34 +02:00
|
|
|
args = {
|
2025-09-11 15:33:53 +00:00
|
|
|
"app_version": app_version,
|
2018-07-02 13:41:09 +02:00
|
|
|
"python_version": sys.version,
|
2019-05-06 19:10:29 +02:00
|
|
|
"os": describe_os_version(),
|
2018-06-12 14:17:34 +02:00
|
|
|
"wallet_type": "unknown",
|
2025-01-21 10:17:12 +01:00
|
|
|
"locale": locale.getlocale()[0] or "?",
|
2018-06-12 14:17:34 +02:00
|
|
|
"description": self.get_user_description()
|
|
|
|
|
}
|
|
|
|
|
try:
|
|
|
|
|
args["wallet_type"] = self.get_wallet_type()
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2018-06-12 14:17:34 +02:00
|
|
|
# Maybe the wallet isn't loaded yet
|
|
|
|
|
pass
|
|
|
|
|
return args
|
|
|
|
|
|
2025-07-18 16:19:54 +00:00
|
|
|
@classmethod
|
|
|
|
|
def _get_traceback_str_to_send(
|
|
|
|
|
cls,
|
|
|
|
|
exctype: type[BaseException],
|
|
|
|
|
excvalue: BaseException,
|
|
|
|
|
tb: TracebackType | None,
|
|
|
|
|
) -> str:
|
2021-07-12 19:24:58 +02:00
|
|
|
# make sure that traceback sent to crash reporter contains
|
|
|
|
|
# e.__context__ and e.__cause__, i.e. if there was a chain of
|
|
|
|
|
# exceptions, we want the full traceback for the whole chain.
|
2025-07-18 16:19:54 +00:00
|
|
|
return "".join(traceback.format_exception(exctype, excvalue, tb))
|
2020-04-18 05:48:11 +02:00
|
|
|
|
2021-07-12 19:24:58 +02:00
|
|
|
def _get_traceback_str_to_display(self) -> str:
|
|
|
|
|
# overridden in Qt subclass
|
2025-07-18 16:19:54 +00:00
|
|
|
return self._get_traceback_str_to_send(*self.exc_args)
|
2021-07-12 19:24:58 +02:00
|
|
|
|
2018-06-12 14:17:34 +02:00
|
|
|
def get_report_string(self):
|
|
|
|
|
info = self.get_additional_info()
|
2021-07-12 19:24:58 +02:00
|
|
|
info["traceback"] = self._get_traceback_str_to_display()
|
2018-06-12 14:17:34 +02:00
|
|
|
return self.issue_template.format(**info)
|
|
|
|
|
|
|
|
|
|
def get_user_description(self):
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
2020-05-01 06:33:38 +02:00
|
|
|
def get_wallet_type(self) -> str:
|
2018-06-12 14:17:34 +02:00
|
|
|
raise NotImplementedError
|
2019-07-04 18:06:21 +02:00
|
|
|
|
|
|
|
|
|
2021-11-05 19:55:22 +01:00
|
|
|
class EarlyExceptionsQueue:
|
|
|
|
|
"""Helper singleton for explicitly sending exceptions to crash reporter.
|
|
|
|
|
|
|
|
|
|
Typically the GUIs set up an "exception hook" that catches all otherwise
|
|
|
|
|
uncaught exceptions (which unroll the stack of a thread completely).
|
|
|
|
|
This class provides methods to report *any* exception, and queueing logic
|
|
|
|
|
that delays processing until the exception hook is set up.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_is_exc_hook_ready = False
|
|
|
|
|
_exc_queue = queue.Queue()
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def set_hook_as_ready(cls):
|
crash reporter: if disabled via config, make sure EEQueue is flushed
If the user had `config.get("show_crash_reporter")==False`, they would
never get to see excs collected via `send_exception_to_crash_reporter(exc)`.
E.g.:
```
E | gui.qt.ElectrumGui | error loading wallet (or creating window for it)
Traceback (most recent call last):
File "/opt/electrum/electrum/gui/qt/__init__.py", line 433, in main
if not self.start_new_window(path, self.config.get('url'), app_is_starting=True):
File "/opt/electrum/electrum/gui/qt/__init__.py", line 307, in wrapper
return func(self, *args, **kwargs)
File "/opt/electrum/electrum/gui/qt/__init__.py", line 332, in start_new_window
wallet = self._start_wizard_to_select_or_create_wallet(path)
File "/opt/electrum/electrum/gui/qt/__init__.py", line 377, in _start_wizard_to_select_or_create_wallet
db = WalletDB(storage.read(), manual_upgrades=False)
File "/opt/electrum/electrum/wallet_db.py", line 73, in __init__
self.load_data(raw)
File "/opt/electrum/electrum/wallet_db.py", line 104, in load_data
self._after_upgrade_tasks()
File "/opt/electrum/electrum/wallet_db.py", line 202, in _after_upgrade_tasks
self._load_transactions()
File "/opt/electrum/electrum/util.py", line 439, in <lambda>
return lambda *args, **kw_args: do_profile(args, kw_args)
File "/opt/electrum/electrum/util.py", line 435, in do_profile
o = func(*args, **kw_args)
File "/opt/electrum/electrum/wallet_db.py", line 1310, in _load_transactions
self.data = StoredDict(self.data, self, [])
File "/opt/electrum/electrum/json_db.py", line 79, in __init__
self.__setitem__(k, v)
File "/opt/electrum/electrum/json_db.py", line 44, in wrapper
return func(self, *args, **kwargs)
File "/opt/electrum/electrum/json_db.py", line 97, in __setitem__
v = self.db._convert_dict(self.path, key, v)
File "/opt/electrum/electrum/wallet_db.py", line 1361, in _convert_dict
v = dict((k, SwapData(**x)) for k, x in v.items())
```
2022-03-15 13:39:33 +01:00
|
|
|
"""Flush the queue and disable it for future exceptions."""
|
2021-11-05 19:55:22 +01:00
|
|
|
if cls._is_exc_hook_ready:
|
|
|
|
|
return
|
|
|
|
|
cls._is_exc_hook_ready = True
|
|
|
|
|
while cls._exc_queue.qsize() > 0:
|
|
|
|
|
e = cls._exc_queue.get()
|
|
|
|
|
cls._send_exception_to_crash_reporter(e)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def send_exception_to_crash_reporter(cls, e: BaseException):
|
|
|
|
|
if cls._is_exc_hook_ready:
|
|
|
|
|
cls._send_exception_to_crash_reporter(e)
|
|
|
|
|
else:
|
|
|
|
|
cls._exc_queue.put(e)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _send_exception_to_crash_reporter(e: BaseException):
|
|
|
|
|
assert EarlyExceptionsQueue._is_exc_hook_ready
|
|
|
|
|
sys.excepthook(type(e), e, e.__traceback__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
send_exception_to_crash_reporter = EarlyExceptionsQueue.send_exception_to_crash_reporter
|
|
|
|
|
|
|
|
|
|
|
2019-07-04 18:06:21 +02:00
|
|
|
def trigger_crash():
|
|
|
|
|
# note: do not change the type of the exception, the message,
|
|
|
|
|
# or the name of this method. All reports generated through this
|
|
|
|
|
# method will be grouped together by the crash reporter, and thus
|
|
|
|
|
# don't spam the issue tracker.
|
|
|
|
|
|
|
|
|
|
class TestingException(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def crash_test():
|
|
|
|
|
raise TestingException("triggered crash for testing purposes")
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
t = threading.Thread(target=crash_test)
|
|
|
|
|
t.start()
|