From 19f17a2bffb423856f309472009263812150a12e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 5 Oct 2020 17:07:33 +0200 Subject: [PATCH] fix main script hanging (not exiting after exception) in some cases Previously an unhandled exception in the main script could cause the main thread to die but the process to hang, as the event loop thread would keep running. example: $ ./run_electrum -o signmessage tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx "mymsg" -w ~/.electrum/testnet/wallets/test_segwit_2 Traceback (most recent call last): File "./run_electrum", line 424, in init_cmdline(config_options, wallet_path, False) File "./run_electrum", line 146, in init_cmdline db = WalletDB(storage.read(), manual_upgrades=False) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 72, in __init__ self.load_data(raw) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 103, in load_data self._after_upgrade_tasks() File "/home/user/wspace/electrum/electrum/wallet_db.py", line 189, in _after_upgrade_tasks self._load_transactions() File "/home/user/wspace/electrum/electrum/util.py", line 406, in return lambda *args, **kw_args: do_profile(args, kw_args) File "/home/user/wspace/electrum/electrum/util.py", line 402, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1139, in _load_transactions self.data = StoredDict(self.data, self, []) File "/home/user/wspace/electrum/electrum/json_db.py", line 79, in __init__ self.__setitem__(k, v) File "/home/user/wspace/electrum/electrum/json_db.py", line 44, in wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/json_db.py", line 105, in __setitem__ v = self.db._convert_dict(self.path, key, v) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1182, in _convert_dict v = dict((k, Invoice.from_json(x)) for k, x in v.items()) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1182, in v = dict((k, Invoice.from_json(x)) for k, x in v.items()) File "/home/user/wspace/electrum/electrum/invoices.py", line 110, in from_json return OnchainInvoice(**x) File "", line 8, in __init__ File "/home/user/wspace/electrum/electrum/invoices.py", line 68, in _decode_outputs output = PartialTxOutput.from_legacy_tuple(*output) File "/home/user/wspace/electrum/electrum/transaction.py", line 131, in from_legacy_tuple return cls.from_address_and_value(addr, val) File "/home/user/wspace/electrum/electrum/transaction.py", line 104, in from_address_and_value return cls(scriptpubkey=bfh(bitcoin.address_to_script(address)), File "/home/user/wspace/electrum/electrum/bitcoin.py", line 422, in address_to_script raise BitcoinException(f"invalid bitcoin address: {addr}") electrum.util.BitcoinException: invalid bitcoin address: tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l --- run_electrum | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/run_electrum b/run_electrum index 67776d94f..776bab5c2 100755 --- a/run_electrum +++ b/run_electrum @@ -37,7 +37,7 @@ if sys.version_info[:3] < _min_python_version_tuple: import warnings import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional script_dir = os.path.dirname(os.path.realpath(__file__)) @@ -95,6 +95,8 @@ from electrum import keystore from electrum.util import create_and_start_event_loop if TYPE_CHECKING: + import threading + from electrum.plugin import Plugins _logger = get_logger(__name__) @@ -113,7 +115,7 @@ def prompt_password(prompt, confirm=True): return password -def init_cmdline(config_options, wallet_path, server): +def init_cmdline(config_options, wallet_path, server, *, config: 'SimpleConfig'): cmdname = config.get('cmd') cmd = known_commands[cmdname] @@ -259,13 +261,20 @@ def init_plugins(config, gui_name): from electrum.plugin import Plugins return Plugins(config, gui_name) + +loop = None # type: Optional[asyncio.AbstractEventLoop] +stop_loop = None # type: Optional[asyncio.Future] +loop_thread = None # type: Optional[threading.Thread] + def sys_exit(i): # stop event loop and exit - loop.call_soon_threadsafe(stop_loop.set_result, 1) - loop_thread.join(timeout=1) + if loop: + loop.call_soon_threadsafe(stop_loop.set_result, 1) + loop_thread.join(timeout=1) sys.exit(i) -if __name__ == '__main__': + +def main(): # The hook will only be used in the Qt GUI right now util.setup_thread_excepthook() # on macOS, delete Process Serial Number arg generated for apps launched in Finder @@ -368,8 +377,21 @@ if __name__ == '__main__': os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) + global loop, stop_loop, loop_thread loop, stop_loop, loop_thread = create_and_start_event_loop() + try: + handle_cmd( + cmdname=cmdname, + config=config, + config_options=config_options, + ) + except Exception: + _logger.exception("") + sys_exit(1) + + +def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): if cmdname == 'gui': configure_logging(config) fd = daemon.get_file_descriptor(config) @@ -404,7 +426,7 @@ if __name__ == '__main__': cmd = known_commands[cmdname] wallet_path = config.get_wallet_path() if not config.get('offline'): - init_cmdline(config_options, wallet_path, True) + init_cmdline(config_options, wallet_path, True, config=config) timeout = config.get('timeout', 60) if timeout: timeout = int(timeout) try: @@ -421,7 +443,7 @@ if __name__ == '__main__': if cmd.requires_network: print_msg("This command cannot be run offline") sys_exit(1) - init_cmdline(config_options, wallet_path, False) + init_cmdline(config_options, wallet_path, False, config=config) plugins = init_plugins(config, 'cmdline') coro = run_offline_command(config, config_options, plugins) fut = asyncio.run_coroutine_threadsafe(coro, loop) @@ -438,3 +460,7 @@ if __name__ == '__main__': elif result is not None: print_msg(json_encode(result)) sys_exit(0) + + +if __name__ == '__main__': + main()