2015-11-30 10:09:54 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
#
|
|
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
|
# Copyright (C) 2015 Thomas Voegtlin
|
|
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# 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:
|
2015-11-30 10:09:54 +01:00
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# The above copyright notice and this permission notice shall be
|
|
|
|
|
# included in all copies or substantial portions of the Software.
|
2015-11-30 10:09:54 +01:00
|
|
|
#
|
2016-02-23 11:36:42 +01:00
|
|
|
# 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
|
2016-01-31 11:43:11 +09:00
|
|
|
import ast
|
2023-10-10 16:57:44 +02:00
|
|
|
import errno
|
2016-01-31 11:43:11 +09:00
|
|
|
import os
|
2016-01-31 14:40:22 +09:00
|
|
|
import time
|
2018-02-15 17:30:40 +01:00
|
|
|
import traceback
|
|
|
|
|
import sys
|
2018-09-25 16:38:26 +02:00
|
|
|
import threading
|
2025-01-23 12:58:28 +01:00
|
|
|
from typing import Dict, Optional, Tuple, Callable, Union, Sequence, Mapping, TYPE_CHECKING
|
2020-02-04 18:17:12 +01:00
|
|
|
from base64 import b64decode, b64encode
|
2020-06-09 17:45:04 +02:00
|
|
|
import json
|
2021-11-05 23:55:55 +01:00
|
|
|
import socket
|
2019-07-30 11:47:17 +02:00
|
|
|
|
2019-12-10 22:55:11 +01:00
|
|
|
import aiohttp
|
|
|
|
|
from aiohttp import web, client_exceptions
|
2025-01-23 12:58:28 +01:00
|
|
|
from aiorpcx import ignore_after
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2020-04-14 16:12:47 +02:00
|
|
|
from . import util
|
2017-01-22 21:25:24 +03:00
|
|
|
from .network import Network
|
2025-01-23 12:58:28 +01:00
|
|
|
from .util import (
|
|
|
|
|
json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword,
|
|
|
|
|
log_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError
|
|
|
|
|
)
|
2018-09-28 17:58:46 +02:00
|
|
|
from .wallet import Wallet, Abstract_Wallet
|
2017-01-22 21:25:24 +03:00
|
|
|
from .storage import WalletStorage
|
2025-01-23 12:58:28 +01:00
|
|
|
from .wallet_db import WalletDB, WalletUnfinished
|
2017-01-22 21:25:24 +03:00
|
|
|
from .commands import known_commands, Commands
|
|
|
|
|
from .simple_config import SimpleConfig
|
|
|
|
|
from .exchange_rate import FxThread
|
2019-07-30 11:47:17 +02:00
|
|
|
from .logging import get_logger, Logger
|
2022-04-11 16:53:25 +02:00
|
|
|
from . import GuiImportError
|
2023-03-29 22:09:46 +00:00
|
|
|
from .plugin import run_hook, Plugins
|
2019-04-26 18:52:26 +02:00
|
|
|
|
2021-03-09 17:52:36 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from electrum import gui
|
|
|
|
|
|
2019-04-26 18:52:26 +02:00
|
|
|
|
|
|
|
|
_logger = get_logger(__name__)
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2019-12-10 22:55:11 +01:00
|
|
|
|
2019-08-15 12:25:59 +02:00
|
|
|
class DaemonNotRunning(Exception):
|
|
|
|
|
pass
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2021-10-25 12:00:00 +00:00
|
|
|
def get_rpcsock_defaultpath(config: SimpleConfig):
|
|
|
|
|
return os.path.join(config.path, 'daemon_rpc_socket')
|
|
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2022-03-02 12:42:49 +01:00
|
|
|
def get_rpcsock_default_type(config: SimpleConfig):
|
2023-05-24 17:41:44 +00:00
|
|
|
if config.RPC_PORT:
|
2022-03-02 12:42:49 +01:00
|
|
|
return 'tcp'
|
2021-11-05 23:55:55 +01:00
|
|
|
# Use unix domain sockets when available,
|
|
|
|
|
# with the extra paranoia that in case windows "implements" them,
|
|
|
|
|
# we want to test it before making it the default there.
|
|
|
|
|
if hasattr(socket, 'AF_UNIX') and sys.platform != 'win32':
|
|
|
|
|
return 'unix'
|
2021-10-25 12:00:00 +00:00
|
|
|
return 'tcp'
|
|
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2018-10-22 16:41:25 +02:00
|
|
|
def get_lockfile(config: SimpleConfig):
|
2016-02-01 10:20:22 +01:00
|
|
|
return os.path.join(config.path, 'daemon')
|
|
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2016-02-01 10:20:22 +01:00
|
|
|
def remove_lockfile(lockfile):
|
|
|
|
|
os.unlink(lockfile)
|
|
|
|
|
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2019-08-15 09:58:23 +02:00
|
|
|
def get_file_descriptor(config: SimpleConfig):
|
2016-02-01 10:20:22 +01:00
|
|
|
'''Tries to create the lockfile, using O_EXCL to
|
2022-02-16 17:40:30 +01:00
|
|
|
prevent races. If it succeeds, it returns the FD.
|
|
|
|
|
Otherwise, try and connect to the server specified in the lockfile.
|
|
|
|
|
If this succeeds, the server is returned. Otherwise, remove the
|
2016-02-01 10:20:22 +01:00
|
|
|
lockfile and try again.'''
|
|
|
|
|
lockfile = get_lockfile(config)
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
2019-08-15 09:58:23 +02:00
|
|
|
return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
2016-02-01 10:20:22 +01:00
|
|
|
except OSError:
|
|
|
|
|
pass
|
2019-08-15 12:25:59 +02:00
|
|
|
try:
|
|
|
|
|
request(config, 'ping')
|
|
|
|
|
return None
|
|
|
|
|
except DaemonNotRunning:
|
|
|
|
|
# Couldn't connect; remove lockfile and try again.
|
|
|
|
|
remove_lockfile(lockfile)
|
2016-02-01 10:20:22 +01:00
|
|
|
|
2017-01-30 12:36:56 +03:00
|
|
|
|
2023-05-24 17:41:44 +00:00
|
|
|
def request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60):
|
2016-02-01 10:20:22 +01:00
|
|
|
lockfile = get_lockfile(config)
|
|
|
|
|
while True:
|
|
|
|
|
create_time = None
|
2023-04-23 02:23:40 +00:00
|
|
|
path = None
|
2016-02-01 10:20:22 +01:00
|
|
|
try:
|
|
|
|
|
with open(lockfile) as f:
|
2021-10-25 12:00:00 +00:00
|
|
|
socktype, address, create_time = ast.literal_eval(f.read())
|
|
|
|
|
if socktype == 'unix':
|
|
|
|
|
path = address
|
|
|
|
|
(host, port) = "127.0.0.1", 0
|
|
|
|
|
# We still need a host and port for e.g. HTTP Host header
|
|
|
|
|
elif socktype == 'tcp':
|
|
|
|
|
(host, port) = address
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"corrupt lockfile; socktype={socktype!r}")
|
2019-08-15 12:25:59 +02:00
|
|
|
except Exception:
|
|
|
|
|
raise DaemonNotRunning()
|
2019-08-15 13:17:16 +02:00
|
|
|
rpc_user, rpc_password = get_rpc_credentials(config)
|
|
|
|
|
server_url = 'http://%s:%d' % (host, port)
|
|
|
|
|
auth = aiohttp.BasicAuth(login=rpc_user, password=rpc_password)
|
asyncio: stop using get_event_loop(). introduce ~singleton loop.
asyncio.get_event_loop() became deprecated in python3.10. (see https://github.com/python/cpython/issues/83710)
```
.../electrum/electrum/daemon.py:470: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
.../electrum/electrum/network.py:276: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
```
Also, according to that thread, "set_event_loop() [... is] not deprecated by oversight".
So, we stop using get_event_loop() and set_event_loop() in our own code.
Note that libraries we use (such as the stdlib for python <3.10), might call get_event_loop,
which then relies on us having called set_event_loop e.g. for the GUI thread. To work around
this, a custom event loop policy providing a get_event_loop implementation is used.
Previously, we have been using a single asyncio event loop, created with
util.create_and_start_event_loop, and code in many places got a reference to this loop
using asyncio.get_event_loop().
Now, we still use a single asyncio event loop, but it is now stored as a global in
util._asyncio_event_loop (access with util.get_asyncio_loop()).
I believe these changes also fix https://github.com/spesmilo/electrum/issues/5376
2022-04-29 18:24:49 +02:00
|
|
|
loop = util.get_asyncio_loop()
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2023-04-23 02:23:40 +00:00
|
|
|
async def request_coroutine(
|
|
|
|
|
*, socktype=socktype, path=path, auth=auth, server_url=server_url, endpoint=endpoint,
|
|
|
|
|
):
|
2021-10-25 12:00:00 +00:00
|
|
|
if socktype == 'unix':
|
|
|
|
|
connector = aiohttp.UnixConnector(path=path)
|
|
|
|
|
elif socktype == 'tcp':
|
|
|
|
|
connector = None # This will transform into TCP.
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"impossible socktype ({socktype!r})")
|
|
|
|
|
async with aiohttp.ClientSession(auth=auth, connector=connector) as session:
|
2020-06-09 17:50:06 +02:00
|
|
|
c = util.JsonRPCClient(session, server_url)
|
2020-06-09 10:05:23 +02:00
|
|
|
return await c.request(endpoint, *args)
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2019-08-15 12:25:59 +02:00
|
|
|
try:
|
2019-08-15 13:17:16 +02:00
|
|
|
fut = asyncio.run_coroutine_threadsafe(request_coroutine(), loop)
|
|
|
|
|
return fut.result(timeout=timeout)
|
|
|
|
|
except aiohttp.client_exceptions.ClientConnectorError as e:
|
|
|
|
|
_logger.info(f"failed to connect to JSON-RPC server {e}")
|
2019-08-15 12:25:59 +02:00
|
|
|
if not create_time or create_time < time.time() - 1.0:
|
|
|
|
|
raise DaemonNotRunning()
|
2016-02-01 10:20:22 +01:00
|
|
|
# Sleep a bit and try again; it might have just been started
|
|
|
|
|
time.sleep(1.0)
|
|
|
|
|
|
|
|
|
|
|
2023-08-03 17:02:40 +00:00
|
|
|
def wait_until_daemon_becomes_ready(*, config: SimpleConfig, timeout=5) -> bool:
|
|
|
|
|
t0 = time.monotonic()
|
|
|
|
|
while True:
|
|
|
|
|
if time.monotonic() > t0 + timeout:
|
|
|
|
|
return False # timeout
|
|
|
|
|
try:
|
|
|
|
|
request(config, 'ping')
|
|
|
|
|
return True # success
|
|
|
|
|
except DaemonNotRunning:
|
|
|
|
|
time.sleep(0.05)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
2018-10-22 16:41:25 +02:00
|
|
|
def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
|
2023-05-24 17:41:44 +00:00
|
|
|
rpc_user = config.RPC_USERNAME or None
|
|
|
|
|
rpc_password = config.RPC_PASSWORD or None
|
2018-01-07 19:26:59 +01:00
|
|
|
if rpc_user is None or rpc_password is None:
|
|
|
|
|
rpc_user = 'user'
|
|
|
|
|
bits = 128
|
|
|
|
|
nbytes = bits // 8 + (bits % 8 > 0)
|
2020-02-04 18:17:12 +01:00
|
|
|
pw_int = randrange(pow(2, bits))
|
|
|
|
|
pw_b64 = b64encode(
|
2018-01-07 19:26:59 +01:00
|
|
|
pw_int.to_bytes(nbytes, 'big'), b'-_')
|
|
|
|
|
rpc_password = to_string(pw_b64, 'ascii')
|
2023-05-24 17:41:44 +00:00
|
|
|
config.RPC_USERNAME = rpc_user
|
|
|
|
|
config.RPC_PASSWORD = rpc_password
|
2018-01-07 19:26:59 +01:00
|
|
|
return rpc_user, rpc_password
|
|
|
|
|
|
|
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticationError(Exception):
|
|
|
|
|
pass
|
2018-10-13 12:11:10 +02:00
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticationInvalidOrMissing(AuthenticationError):
|
|
|
|
|
pass
|
|
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticationCredentialsInvalid(AuthenticationError):
|
|
|
|
|
pass
|
|
|
|
|
|
2025-01-23 12:58:28 +01:00
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
class AuthenticatedServer(Logger):
|
|
|
|
|
|
|
|
|
|
def __init__(self, rpc_user, rpc_password):
|
2019-07-30 11:47:17 +02:00
|
|
|
Logger.__init__(self)
|
2020-05-12 10:02:22 +02:00
|
|
|
self.rpc_user = rpc_user
|
|
|
|
|
self.rpc_password = rpc_password
|
|
|
|
|
self.auth_lock = asyncio.Lock()
|
2020-06-09 17:45:04 +02:00
|
|
|
self._methods = {} # type: Dict[str, Callable]
|
|
|
|
|
|
|
|
|
|
def register_method(self, f):
|
|
|
|
|
assert f.__name__ not in self._methods, f"name collision for {f.__name__}"
|
|
|
|
|
self._methods[f.__name__] = f
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
async def authenticate(self, headers):
|
|
|
|
|
if self.rpc_password == '':
|
|
|
|
|
# RPC authentication is disabled
|
|
|
|
|
return
|
|
|
|
|
auth_string = headers.get('Authorization', None)
|
|
|
|
|
if auth_string is None:
|
|
|
|
|
raise AuthenticationInvalidOrMissing('CredentialsMissing')
|
|
|
|
|
basic, _, encoded = auth_string.partition(' ')
|
|
|
|
|
if basic != 'Basic':
|
|
|
|
|
raise AuthenticationInvalidOrMissing('UnsupportedType')
|
|
|
|
|
encoded = to_bytes(encoded, 'utf8')
|
|
|
|
|
credentials = to_string(b64decode(encoded), 'utf8')
|
|
|
|
|
username, _, password = credentials.partition(':')
|
|
|
|
|
if not (constant_time_compare(username, self.rpc_user)
|
|
|
|
|
and constant_time_compare(password, self.rpc_password)):
|
|
|
|
|
await asyncio.sleep(0.050)
|
|
|
|
|
raise AuthenticationCredentialsInvalid('Invalid Credentials')
|
|
|
|
|
|
|
|
|
|
async def handle(self, request):
|
|
|
|
|
async with self.auth_lock:
|
|
|
|
|
try:
|
|
|
|
|
await self.authenticate(request.headers)
|
|
|
|
|
except AuthenticationInvalidOrMissing:
|
|
|
|
|
return web.Response(headers={"WWW-Authenticate": "Basic realm=Electrum"},
|
|
|
|
|
text='Unauthorized', status=401)
|
|
|
|
|
except AuthenticationCredentialsInvalid:
|
|
|
|
|
return web.Response(text='Forbidden', status=403)
|
2020-06-09 10:05:23 +02:00
|
|
|
try:
|
|
|
|
|
request = await request.text()
|
|
|
|
|
request = json.loads(request)
|
|
|
|
|
method = request['method']
|
|
|
|
|
_id = request['id']
|
2020-06-09 17:45:04 +02:00
|
|
|
params = request.get('params', []) # type: Union[Sequence, Mapping]
|
|
|
|
|
if method not in self._methods:
|
|
|
|
|
raise Exception(f"attempting to use unregistered method: {method}")
|
|
|
|
|
f = self._methods[method]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.exception("invalid request")
|
2020-06-09 10:05:23 +02:00
|
|
|
return web.Response(text='Invalid Request', status=500)
|
2020-09-23 21:39:31 +02:00
|
|
|
response = {
|
|
|
|
|
'id': _id,
|
|
|
|
|
'jsonrpc': '2.0',
|
|
|
|
|
}
|
2020-06-09 10:05:23 +02:00
|
|
|
try:
|
2020-06-09 17:45:04 +02:00
|
|
|
if isinstance(params, dict):
|
|
|
|
|
response['result'] = await f(**params)
|
|
|
|
|
else:
|
|
|
|
|
response['result'] = await f(*params)
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
except UserFacingException as e:
|
|
|
|
|
response['error'] = {
|
|
|
|
|
'code': JsonRPCError.Codes.USERFACING,
|
|
|
|
|
'message': str(e),
|
|
|
|
|
}
|
2020-06-09 10:05:23 +02:00
|
|
|
except BaseException as e:
|
2020-06-09 17:45:04 +02:00
|
|
|
self.logger.exception("internal error while executing RPC")
|
2020-11-14 19:59:59 +01:00
|
|
|
response['error'] = {
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
'code': JsonRPCError.Codes.INTERNAL,
|
|
|
|
|
'message': "internal error while executing RPC",
|
|
|
|
|
'data': {
|
|
|
|
|
"exception": repr(e),
|
2025-01-10 13:26:39 +00:00
|
|
|
"traceback": "".join(traceback.format_exception(e)),
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
},
|
2020-11-14 19:59:59 +01:00
|
|
|
}
|
2020-06-09 10:05:23 +02:00
|
|
|
return web.json_response(response)
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CommandsServer(AuthenticatedServer):
|
|
|
|
|
|
2023-05-24 17:41:44 +00:00
|
|
|
def __init__(self, daemon: 'Daemon', fd):
|
2020-05-12 10:02:22 +02:00
|
|
|
rpc_user, rpc_password = get_rpc_credentials(daemon.config)
|
|
|
|
|
AuthenticatedServer.__init__(self, rpc_user, rpc_password)
|
|
|
|
|
self.daemon = daemon
|
|
|
|
|
self.fd = fd
|
|
|
|
|
self.config = daemon.config
|
2023-05-24 17:41:44 +00:00
|
|
|
sockettype = self.config.RPC_SOCKET_TYPE
|
2022-03-02 12:42:49 +01:00
|
|
|
self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)
|
2023-05-24 17:41:44 +00:00
|
|
|
self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config)
|
|
|
|
|
self.host = self.config.RPC_HOST
|
|
|
|
|
self.port = self.config.RPC_PORT
|
2020-05-12 10:02:22 +02:00
|
|
|
self.app = web.Application()
|
|
|
|
|
self.app.router.add_post("/", self.handle)
|
2020-06-09 17:45:04 +02:00
|
|
|
self.register_method(self.ping)
|
|
|
|
|
self.register_method(self.gui)
|
2020-05-12 10:02:22 +02:00
|
|
|
self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon)
|
|
|
|
|
for cmdname in known_commands:
|
2020-06-09 17:45:04 +02:00
|
|
|
self.register_method(getattr(self.cmd_runner, cmdname))
|
|
|
|
|
self.register_method(self.run_cmdline)
|
2020-05-12 10:02:22 +02:00
|
|
|
|
daemon: better error msg if rpchost/rpcport is badly configured
old traceback:
```
$ ./run_electrum --testnet -o setconfig rpchost qweasdfcsdf
$ ./run_electrum --testnet -o setconfig rpcport 7777
$ ./run_electrum --testnet daemon
E | daemon.Daemon | taskgroup died.
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 419, in _run
async with self.taskgroup as group:
File "/home/user/wspace/electrum/packages/aiorpcx/curio.py", line 297, in __aexit__
await self.join()
File "/home/user/wspace/electrum/electrum/util.py", line 1335, in join
task.result()
File "/home/user/wspace/electrum/electrum/daemon.py", line 281, in run
await site.start() #
File "/home/user/wspace/electrum/packages/aiohttp/web_runner.py", line 121, in start
self._server = await loop.create_server(
File "/usr/lib/python3.10/asyncio/base_events.py", line 1471, in create_server
infos = await tasks.gather(*fs)
File "/usr/lib/python3.10/asyncio/base_events.py", line 1408, in _create_server_getaddrinfo
infos = await self._ensure_resolved((host, port), family=family,
File "/usr/lib/python3.10/asyncio/base_events.py", line 1404, in _ensure_resolved
return await loop.getaddrinfo(host, port, family=family, type=type,
File "/usr/lib/python3.10/asyncio/base_events.py", line 860, in getaddrinfo
return await self.run_in_executor(
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/lib/python3.10/socket.py", line 955, in getaddrinfo
for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno -3] Temporary failure in name resolution
```
2022-10-29 03:14:12 +00:00
|
|
|
def _socket_config_str(self) -> str:
|
|
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
return f"<socket type={self.socktype}, path={self.sockpath}>"
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
return f"<socket type={self.socktype}, host={self.host}, port={self.port}>"
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"unknown socktype '{self.socktype!r}'")
|
|
|
|
|
|
2020-05-12 10:02:22 +02:00
|
|
|
async def run(self):
|
|
|
|
|
self.runner = web.AppRunner(self.app)
|
|
|
|
|
await self.runner.setup()
|
2021-10-25 12:00:00 +00:00
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
site = web.UnixSite(self.runner, self.sockpath)
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
site = web.TCPSite(self.runner, self.host, self.port)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"unknown socktype '{self.socktype!r}'")
|
daemon: better error msg if rpchost/rpcport is badly configured
old traceback:
```
$ ./run_electrum --testnet -o setconfig rpchost qweasdfcsdf
$ ./run_electrum --testnet -o setconfig rpcport 7777
$ ./run_electrum --testnet daemon
E | daemon.Daemon | taskgroup died.
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 419, in _run
async with self.taskgroup as group:
File "/home/user/wspace/electrum/packages/aiorpcx/curio.py", line 297, in __aexit__
await self.join()
File "/home/user/wspace/electrum/electrum/util.py", line 1335, in join
task.result()
File "/home/user/wspace/electrum/electrum/daemon.py", line 281, in run
await site.start() #
File "/home/user/wspace/electrum/packages/aiohttp/web_runner.py", line 121, in start
self._server = await loop.create_server(
File "/usr/lib/python3.10/asyncio/base_events.py", line 1471, in create_server
infos = await tasks.gather(*fs)
File "/usr/lib/python3.10/asyncio/base_events.py", line 1408, in _create_server_getaddrinfo
infos = await self._ensure_resolved((host, port), family=family,
File "/usr/lib/python3.10/asyncio/base_events.py", line 1404, in _ensure_resolved
return await loop.getaddrinfo(host, port, family=family, type=type,
File "/usr/lib/python3.10/asyncio/base_events.py", line 860, in getaddrinfo
return await self.run_in_executor(
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/lib/python3.10/socket.py", line 955, in getaddrinfo
for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
socket.gaierror: [Errno -3] Temporary failure in name resolution
```
2022-10-29 03:14:12 +00:00
|
|
|
try:
|
|
|
|
|
await site.start()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise Exception(f"failed to start CommandsServer at {self._socket_config_str()}. got exc: {e!r}") from None
|
2020-05-12 10:02:22 +02:00
|
|
|
socket = site._server.sockets[0]
|
2021-10-25 12:00:00 +00:00
|
|
|
if self.socktype == 'unix':
|
|
|
|
|
addr = self.sockpath
|
|
|
|
|
elif self.socktype == 'tcp':
|
|
|
|
|
addr = socket.getsockname()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"impossible socktype ({self.socktype!r})")
|
|
|
|
|
os.write(self.fd, bytes(repr((self.socktype, addr, time.time())), 'utf8'))
|
2020-05-12 10:02:22 +02:00
|
|
|
os.close(self.fd)
|
2021-11-15 17:43:34 +01:00
|
|
|
self.logger.info(f"now running and listening. socktype={self.socktype}, addr={addr}")
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
async def ping(self):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
async def gui(self, config_options):
|
2023-02-06 17:55:56 +00:00
|
|
|
# note: "config_options" is coming from the short-lived CLI-invocation,
|
|
|
|
|
# while self.config is the config of the long-lived daemon process.
|
|
|
|
|
# "config_options" should have priority.
|
2020-05-12 10:02:22 +02:00
|
|
|
if self.daemon.gui_object:
|
|
|
|
|
if hasattr(self.daemon.gui_object, 'new_window'):
|
2025-03-18 19:33:24 +00:00
|
|
|
if config_options.get(SimpleConfig.NETWORK_OFFLINE.key()) and not self.config.NETWORK_OFFLINE:
|
|
|
|
|
raise UserFacingException(
|
|
|
|
|
"error: current GUI is running online, so it cannot open a new wallet offline.")
|
2023-02-06 17:55:56 +00:00
|
|
|
path = config_options.get('wallet_path') or self.config.get_wallet_path(use_gui_last_wallet=True)
|
2020-05-12 10:02:22 +02:00
|
|
|
self.daemon.gui_object.new_window(path, config_options.get('url'))
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
return True
|
2020-05-12 10:02:22 +02:00
|
|
|
else:
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
raise UserFacingException("error: current GUI does not support multiple windows")
|
2020-05-12 10:02:22 +02:00
|
|
|
else:
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
raise UserFacingException("error: Electrum is running in daemon mode. Please stop the daemon first.")
|
2020-05-12 10:02:22 +02:00
|
|
|
|
|
|
|
|
async def run_cmdline(self, config_options):
|
|
|
|
|
cmdname = config_options['cmd']
|
2025-03-06 11:43:50 +01:00
|
|
|
cmd = known_commands.get(cmdname)
|
|
|
|
|
if not cmd:
|
|
|
|
|
return f"unknown command: {cmdname}"
|
2020-05-12 10:02:22 +02:00
|
|
|
# arguments passed to function
|
|
|
|
|
args = [config_options.get(x) for x in cmd.params]
|
|
|
|
|
# decode json arguments
|
|
|
|
|
args = [json_decode(i) for i in args]
|
|
|
|
|
# options
|
|
|
|
|
kwargs = {}
|
|
|
|
|
for x in cmd.options:
|
|
|
|
|
kwargs[x] = config_options.get(x)
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
if 'wallet_path' in cmd.options:
|
2020-05-12 10:02:22 +02:00
|
|
|
kwargs['wallet_path'] = config_options.get('wallet_path')
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
elif 'wallet' in cmd.options:
|
|
|
|
|
kwargs['wallet'] = config_options.get('wallet_path')
|
2020-05-12 10:02:22 +02:00
|
|
|
func = getattr(self.cmd_runner, cmd.name)
|
cli/rpc: nicer error messages and error-passing
Previously, generally, in case of any error, commands would raise a generic "Exception()" and the CLI/RPC would convert that and return it as `str(e)`.
With this change, we now distinguish "user-facing exceptions" (e.g. "Password required" or "wallet not loaded") and "internal errors" (e.g. bugs).
- for "user-facing exceptions", the behaviour is unchanged
- for "internal errors", we now pass around the traceback (e.g. from daemon server to rpc client) and show it to the user (previously, assuming there was a daemon running, the user could only retrieve the exception from the log of that daemon). These errors use a new jsonrpc error code int (code 2).
As the logic only changes for "internal errors", I deem this change not to be compatibility-breaking.
----------
Examples follow.
Consider the following two commands:
```
@command('')
async def errorgood(self):
from electrum.util import UserFacingException
raise UserFacingException("heyheyhey")
@command('')
async def errorbad(self):
raise Exception("heyheyhey")
```
----------
(before change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9221)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
heyheyhey
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
```
----------
(after change)
CLI with daemon:
```
$ ./run_electrum --testnet daemon -d
starting daemon (PID 9254)
$ ./run_electrum --testnet errorgood
heyheyhey
$ ./run_electrum --testnet errorbad
(inside daemon): Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/daemon.py", line 254, in handle
response['result'] = await f(*params)
File "/home/user/wspace/electrum/electrum/daemon.py", line 361, in run_cmdline
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
internal error while executing RPC
$ ./run_electrum --testnet stop
Daemon stopped
```
CLI without daemon:
```
$ ./run_electrum --testnet -o errorgood
heyheyhey
$ ./run_electrum --testnet -o errorbad
0.78 | E | __main__ | error running command (without daemon)
Traceback (most recent call last):
File "/home/user/wspace/electrum/./run_electrum", line 534, in handle_cmd
result = fut.result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/usr/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/home/user/wspace/electrum/./run_electrum", line 255, in run_offline_command
result = await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 163, in func_wrapper
return await func(*args, **kwargs)
File "/home/user/wspace/electrum/electrum/commands.py", line 217, in errorbad
raise Exception("heyheyhey")
Exception: heyheyhey
```
RPC:
```
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorgood","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 1, "message": "heyheyhey"}}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"errorbad","params":[]}' http://user:pass@127.0.0.1:7777
{"id": "curltext", "jsonrpc": "2.0", "error": {"code": 2, "message": "internal error while executing RPC", "data": {"exception": "Exception('heyheyhey')", "traceback": "Traceback (most recent call last):\n File \"/home/user/wspace/electrum/electrum/daemon.py\", line 254, in handle\n response['result'] = await f(*params)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 163, in func_wrapper\n return await func(*args, **kwargs)\n File \"/home/user/wspace/electrum/electrum/commands.py\", line 217, in errorbad\n raise Exception(\"heyheyhey\")\nException: heyheyhey\n"}}}
```
2024-02-12 19:02:02 +00:00
|
|
|
# execute requested command now. note: cmd can raise, the caller (self.handle) will wrap it.
|
|
|
|
|
result = await func(*args, **kwargs)
|
2020-05-12 10:02:22 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
class Daemon(Logger):
|
2015-11-30 10:09:54 +01:00
|
|
|
|
2023-03-29 22:09:46 +00:00
|
|
|
network: Optional[Network] = None
|
|
|
|
|
gui_object: Optional['gui.BaseElectrumGui'] = None
|
2020-04-14 16:56:17 +02:00
|
|
|
|
2018-11-16 14:39:22 +01:00
|
|
|
@profiler
|
2023-03-29 22:09:46 +00:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
config: SimpleConfig,
|
|
|
|
|
fd=None,
|
|
|
|
|
*,
|
|
|
|
|
listen_jsonrpc: bool = True,
|
|
|
|
|
start_network: bool = True, # setting to False allows customising network settings before starting it
|
|
|
|
|
):
|
2019-08-15 13:17:16 +02:00
|
|
|
Logger.__init__(self)
|
2015-11-30 10:09:54 +01:00
|
|
|
self.config = config
|
2021-07-07 16:57:09 +03:00
|
|
|
self.listen_jsonrpc = listen_jsonrpc
|
2018-10-01 17:56:51 +02:00
|
|
|
if fd is None and listen_jsonrpc:
|
2019-08-15 09:58:23 +02:00
|
|
|
fd = get_file_descriptor(config)
|
|
|
|
|
if fd is None:
|
|
|
|
|
raise Exception('failed to lock daemon; already running?')
|
2020-10-05 18:02:37 +02:00
|
|
|
if 'wallet_path' in config.cmdline_options:
|
|
|
|
|
self.logger.warning("Ignoring parameter 'wallet_path' for daemon. "
|
|
|
|
|
"Use the load_wallet command instead.")
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
self._plugins = None # type: Optional[Plugins]
|
asyncio: stop using get_event_loop(). introduce ~singleton loop.
asyncio.get_event_loop() became deprecated in python3.10. (see https://github.com/python/cpython/issues/83710)
```
.../electrum/electrum/daemon.py:470: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
.../electrum/electrum/network.py:276: DeprecationWarning: There is no current event loop
self.asyncio_loop = asyncio.get_event_loop()
```
Also, according to that thread, "set_event_loop() [... is] not deprecated by oversight".
So, we stop using get_event_loop() and set_event_loop() in our own code.
Note that libraries we use (such as the stdlib for python <3.10), might call get_event_loop,
which then relies on us having called set_event_loop e.g. for the GUI thread. To work around
this, a custom event loop policy providing a get_event_loop implementation is used.
Previously, we have been using a single asyncio event loop, created with
util.create_and_start_event_loop, and code in many places got a reference to this loop
using asyncio.get_event_loop().
Now, we still use a single asyncio event loop, but it is now stored as a global in
util._asyncio_event_loop (access with util.get_asyncio_loop()).
I believe these changes also fix https://github.com/spesmilo/electrum/issues/5376
2022-04-29 18:24:49 +02:00
|
|
|
self.asyncio_loop = util.get_asyncio_loop()
|
2023-05-24 17:41:44 +00:00
|
|
|
if not self.config.NETWORK_OFFLINE:
|
2020-01-09 17:50:05 +01:00
|
|
|
self.network = Network(config, daemon=self)
|
2023-03-28 15:45:15 +00:00
|
|
|
self.fx = FxThread(config=config)
|
2023-06-30 10:49:26 +00:00
|
|
|
# wallet_key -> wallet
|
2019-09-09 22:15:11 +02:00
|
|
|
self._wallets = {} # type: Dict[str, Abstract_Wallet]
|
2022-07-06 18:33:08 +02:00
|
|
|
self._wallet_lock = threading.RLock()
|
2020-01-09 17:50:05 +01:00
|
|
|
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stop_entered = False
|
|
|
|
|
self._stopping_soon_or_errored = threading.Event()
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event = threading.Event()
|
2023-08-03 15:19:19 +00:00
|
|
|
|
2022-02-08 12:34:49 +01:00
|
|
|
self.taskgroup = OldTaskGroup()
|
2023-08-03 15:19:19 +00:00
|
|
|
asyncio.run_coroutine_threadsafe(self._run(), self.asyncio_loop)
|
2023-03-29 22:09:46 +00:00
|
|
|
if start_network and self.network:
|
|
|
|
|
self.start_network()
|
2023-08-03 15:19:19 +00:00
|
|
|
# Setup commands server
|
|
|
|
|
self.commands_server = None
|
|
|
|
|
if listen_jsonrpc:
|
|
|
|
|
self.commands_server = CommandsServer(self, fd)
|
|
|
|
|
asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.commands_server.run()), self.asyncio_loop)
|
2020-01-09 17:50:05 +01:00
|
|
|
|
|
|
|
|
@log_exceptions
|
2023-08-03 15:19:19 +00:00
|
|
|
async def _run(self):
|
2020-02-27 20:22:49 +01:00
|
|
|
self.logger.info("starting taskgroup.")
|
2020-01-09 17:50:05 +01:00
|
|
|
try:
|
|
|
|
|
async with self.taskgroup as group:
|
2020-01-11 00:04:00 +01:00
|
|
|
await group.spawn(asyncio.Event().wait) # run forever (until cancel)
|
2020-02-27 20:22:49 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.exception("taskgroup died.")
|
2021-11-05 18:33:03 +01:00
|
|
|
util.send_exception_to_crash_reporter(e)
|
2020-01-09 17:50:05 +01:00
|
|
|
finally:
|
2020-02-27 20:22:49 +01:00
|
|
|
self.logger.info("taskgroup stopped.")
|
2021-11-05 18:33:03 +01:00
|
|
|
# note: we could just "await self.stop()", but in that case GUI users would
|
|
|
|
|
# not see the exception (especially if the GUI did not start yet).
|
|
|
|
|
self._stopping_soon_or_errored.set()
|
2016-08-15 08:14:19 +02:00
|
|
|
|
2023-03-29 22:09:46 +00:00
|
|
|
def start_network(self):
|
|
|
|
|
self.logger.info(f"starting network.")
|
2023-05-24 17:41:44 +00:00
|
|
|
assert not self.config.NETWORK_OFFLINE
|
2023-03-29 22:09:46 +00:00
|
|
|
assert self.network
|
|
|
|
|
self.network.start(jobs=[self.fx.run])
|
|
|
|
|
# prepare lightning functionality, also load channel db early
|
2023-05-24 17:41:44 +00:00
|
|
|
if self.config.LIGHTNING_USE_GOSSIP:
|
2023-03-29 22:09:46 +00:00
|
|
|
self.network.start_gossip()
|
|
|
|
|
|
2023-06-30 10:49:26 +00:00
|
|
|
@staticmethod
|
|
|
|
|
def _wallet_key_from_path(path) -> str:
|
|
|
|
|
"""This does stricter path standardization than 'standardize_path'.
|
|
|
|
|
It is used for keying the _wallets dict, but not for the actual filesystem operations. (see #8495)
|
|
|
|
|
"""
|
|
|
|
|
path = standardize_path(path)
|
|
|
|
|
# also resolve symlinks and windows network mounts/etc:
|
|
|
|
|
path = os.path.realpath(path)
|
|
|
|
|
path = os.path.normcase(path)
|
|
|
|
|
return str(path)
|
|
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
def with_wallet_lock(func):
|
|
|
|
|
def func_wrapper(self: 'Daemon', *args, **kwargs):
|
|
|
|
|
with self._wallet_lock:
|
|
|
|
|
return func(self, *args, **kwargs)
|
|
|
|
|
return func_wrapper
|
|
|
|
|
|
|
|
|
|
@with_wallet_lock
|
2023-09-22 11:49:53 +02:00
|
|
|
def load_wallet(self, path, password, *, upgrade=False) -> Optional[Abstract_Wallet]:
|
2019-02-08 12:59:06 +01:00
|
|
|
path = standardize_path(path)
|
2023-06-30 10:49:26 +00:00
|
|
|
wallet_key = self._wallet_key_from_path(path)
|
2016-07-02 08:58:56 +02:00
|
|
|
# wizard will be launched if we return
|
2023-06-30 10:49:26 +00:00
|
|
|
if wallet := self._wallets.get(wallet_key):
|
2016-06-20 16:25:11 +02:00
|
|
|
return wallet
|
2023-09-22 11:49:53 +02:00
|
|
|
wallet = self._load_wallet(path, password, upgrade=upgrade, config=self.config)
|
2025-02-13 11:52:46 +01:00
|
|
|
if wallet.requires_unlock() and password is not None:
|
2024-12-18 10:40:49 +01:00
|
|
|
wallet.unlock(password)
|
2022-07-06 18:33:08 +02:00
|
|
|
wallet.start_network(self.network)
|
2023-01-13 00:58:02 +00:00
|
|
|
self.add_wallet(wallet)
|
2024-10-13 11:06:14 +02:00
|
|
|
self.update_recently_opened_wallets(path)
|
2022-07-06 18:33:08 +02:00
|
|
|
return wallet
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2023-02-08 23:36:36 +00:00
|
|
|
@profiler
|
2022-07-06 18:33:08 +02:00
|
|
|
def _load_wallet(
|
|
|
|
|
path,
|
|
|
|
|
password,
|
|
|
|
|
*,
|
2023-09-22 11:49:53 +02:00
|
|
|
upgrade: bool = False,
|
2022-07-06 18:33:08 +02:00
|
|
|
config: SimpleConfig,
|
|
|
|
|
) -> Optional[Abstract_Wallet]:
|
|
|
|
|
path = standardize_path(path)
|
2020-02-05 15:13:37 +01:00
|
|
|
storage = WalletStorage(path)
|
2017-03-06 08:33:35 +01:00
|
|
|
if not storage.file_exists():
|
2023-10-10 16:57:44 +02:00
|
|
|
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
2017-02-09 17:08:27 +01:00
|
|
|
if storage.is_encrypted():
|
|
|
|
|
if not password:
|
2023-10-10 16:57:44 +02:00
|
|
|
raise InvalidPassword('No password given')
|
2017-03-06 08:33:35 +01:00
|
|
|
storage.decrypt(password)
|
2020-02-05 15:13:37 +01:00
|
|
|
# read data, pass it to db
|
2023-10-10 16:57:44 +02:00
|
|
|
db = WalletDB(storage.read(), storage=storage, upgrade=upgrade)
|
2020-02-05 15:13:37 +01:00
|
|
|
if db.get_action():
|
2023-10-10 16:57:44 +02:00
|
|
|
raise WalletUnfinished(db)
|
2023-06-23 17:36:34 +02:00
|
|
|
wallet = Wallet(db, config=config)
|
2015-11-30 10:09:54 +01:00
|
|
|
return wallet
|
|
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2019-09-09 22:15:11 +02:00
|
|
|
def add_wallet(self, wallet: Abstract_Wallet) -> None:
|
2016-06-20 16:25:11 +02:00
|
|
|
path = wallet.storage.path
|
2023-06-30 10:49:26 +00:00
|
|
|
wallet_key = self._wallet_key_from_path(path)
|
|
|
|
|
self._wallets[wallet_key] = wallet
|
2023-01-13 00:58:02 +00:00
|
|
|
run_hook('daemon_wallet_loaded', self, wallet)
|
2016-06-20 16:25:11 +02:00
|
|
|
|
commands: make 'wallet'-mangling in decorator less obscure, and fixes
- some commands expect a 'wallet_path' arg, while others expect 'wallet'
- 'wallet_path' in the end is supposed to be a str,
'wallet' in the end is supposed to be an Optional[Abstract_Wallet]
- initially, in the decorator, 'wallet' can be a str, in which case
the decorator replaces it with an Abstract_Wallet (from the daemon)
- Previously the decorator sometimes converted 'wallet_path' to 'wallet'.
This was because when called from the CLI it was always given 'wallet_path' (and never 'wallet),
while when called from JSON-RPC it was given either 'wallet' or 'wallet_path' (depending on command).
Now, the CLI also behaves as JSON-RPC, and hence 'wallet_path' and 'wallet' are fully separate.
- A bug is fixed where, when a command that only optionally takes a 'wallet' (such as gettransaction),
was called from the JSON-RPC with the arg present, it raised; and when called from CLI with the arg present
the arg was not actually passed to the command.
- A bug is fixed where if one command calls another command (that both take a 'wallet'),
it would raise (due to assuming 'wallet' is str and needs to be converted to Abstract_Wallet).
This fixes #6154.
-----
$ ./run_electrum --testnet daemon -d
$ ./run_electrum --testnet load_wallet -w ~/.electrum/testnet/wallets/default_wallet
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "result": "0200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fdfe0000483045022100e2a508bb78c2172eb03f081a342454ba1d24669e959700973b1a742a4fedd0c302203174e06feda265031cf9aa0364d4a4eafb71b0c0a62e76be7795cfbb307b677a01483045022100d0e14564838fac754395158741d64c73da2b86e7900dfdc6a63c7492b232ba130220778e7e7c21d94ebcd340057302aeff7e9a797a3aa3e0ac4884e9ff27339ea6e9014c69522102091f0b4d8ab30016a5d1c088249e02883fad8160f06fa53588ad8598650a3e6221035f2f8263bb3608d6cc4ee03bd4cb8d65c4d70af71049f05fbfee4978832a1fd22103fe42dab58718ea0413f7c8de693cdeee22ce19b1dc34c0bbdd7a48245465c5a253aefdffffff01cb9f0700000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac802c1800", "id": "curltext"}
$ curl --data-binary '{"id":"curltext","jsonrpc":"2.0","method":"gettransaction","params":{"txid":"9f43ff71ea2594873e4e7d15e61254a3661ff2df1af76325c854d9aa199550ce", "wallet":"~/.electrum/testnet/wallets/default_wallet"}}' http://user:pass@127.0.0.1:7777
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "'str' object has no attribute 'db'"}, "id": "curltext"}
2020-05-14 16:33:02 +02:00
|
|
|
def get_wallet(self, path: str) -> Optional[Abstract_Wallet]:
|
2023-06-30 10:49:26 +00:00
|
|
|
wallet_key = self._wallet_key_from_path(path)
|
|
|
|
|
return self._wallets.get(wallet_key)
|
2017-03-05 13:30:57 +01:00
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2019-09-09 22:15:11 +02:00
|
|
|
def get_wallets(self) -> Dict[str, Abstract_Wallet]:
|
|
|
|
|
return dict(self._wallets) # copy
|
|
|
|
|
|
|
|
|
|
def delete_wallet(self, path: str) -> bool:
|
2019-02-08 11:17:48 +01:00
|
|
|
self.stop_wallet(path)
|
|
|
|
|
if os.path.exists(path):
|
|
|
|
|
os.unlink(path)
|
2024-10-13 11:06:14 +02:00
|
|
|
self.update_recently_opened_wallets(path, remove=True)
|
2019-02-08 11:17:48 +01:00
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
2019-09-09 22:15:11 +02:00
|
|
|
def stop_wallet(self, path: str) -> bool:
|
2021-06-17 12:35:31 +02:00
|
|
|
"""Returns True iff a wallet was found."""
|
|
|
|
|
# note: this must not be called from the event loop. # TODO raise if so
|
|
|
|
|
fut = asyncio.run_coroutine_threadsafe(self._stop_wallet(path), self.asyncio_loop)
|
|
|
|
|
return fut.result()
|
|
|
|
|
|
2022-07-06 18:33:08 +02:00
|
|
|
@with_wallet_lock
|
2021-06-17 12:35:31 +02:00
|
|
|
async def _stop_wallet(self, path: str) -> bool:
|
2019-09-04 20:16:47 +02:00
|
|
|
"""Returns True iff a wallet was found."""
|
2023-06-30 10:49:26 +00:00
|
|
|
wallet_key = self._wallet_key_from_path(path)
|
|
|
|
|
wallet = self._wallets.pop(wallet_key, None)
|
2019-09-04 20:16:47 +02:00
|
|
|
if not wallet:
|
|
|
|
|
return False
|
2021-06-17 12:35:31 +02:00
|
|
|
await wallet.stop()
|
2019-09-04 20:16:47 +02:00
|
|
|
return True
|
2016-06-20 16:25:11 +02:00
|
|
|
|
2019-08-15 13:17:16 +02:00
|
|
|
def run_daemon(self):
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
# init plugins
|
|
|
|
|
self._plugins = Plugins(self.config, 'cmdline')
|
|
|
|
|
# block until we are stopping
|
2019-08-15 13:17:16 +02:00
|
|
|
try:
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stopping_soon_or_errored.wait()
|
2019-08-15 13:17:16 +02:00
|
|
|
except KeyboardInterrupt:
|
2022-10-29 02:58:09 +00:00
|
|
|
self.logger.info("got KeyboardInterrupt")
|
|
|
|
|
# we either initiate shutdown now,
|
|
|
|
|
# or it has already been initiated (in which case this is a no-op):
|
|
|
|
|
self.logger.info("run_daemon is calling stop()")
|
|
|
|
|
asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()
|
|
|
|
|
# wait until "stop" finishes:
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event.wait()
|
2019-08-15 13:17:16 +02:00
|
|
|
|
2021-03-17 19:16:07 +01:00
|
|
|
async def stop(self):
|
2021-11-05 18:33:03 +01:00
|
|
|
if self._stop_entered:
|
2021-11-05 17:24:03 +01:00
|
|
|
return
|
2021-11-05 18:33:03 +01:00
|
|
|
self._stop_entered = True
|
|
|
|
|
self._stopping_soon_or_errored.set()
|
2021-11-05 17:24:03 +01:00
|
|
|
self.logger.info("stop() entered. initiating shutdown")
|
2021-03-17 19:16:07 +01:00
|
|
|
try:
|
|
|
|
|
if self.gui_object:
|
|
|
|
|
self.gui_object.stop()
|
2021-11-05 17:24:03 +01:00
|
|
|
self.logger.info("stopping all wallets")
|
2022-02-08 12:34:49 +01:00
|
|
|
async with OldTaskGroup() as group:
|
2021-11-05 17:24:03 +01:00
|
|
|
for k, wallet in self._wallets.items():
|
|
|
|
|
await group.spawn(wallet.stop())
|
|
|
|
|
self.logger.info("stopping network and taskgroup")
|
|
|
|
|
async with ignore_after(2):
|
2022-02-08 12:34:49 +01:00
|
|
|
async with OldTaskGroup() as group:
|
2021-11-05 17:24:03 +01:00
|
|
|
if self.network:
|
|
|
|
|
await group.spawn(self.network.stop(full_shutdown=True))
|
|
|
|
|
await group.spawn(self.taskgroup.cancel_remaining())
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
if self._plugins:
|
|
|
|
|
self.logger.info("stopping plugins")
|
|
|
|
|
self._plugins.stop()
|
|
|
|
|
async with ignore_after(1):
|
|
|
|
|
await self._plugins.stopped_event_async.wait()
|
2021-03-17 19:16:07 +01:00
|
|
|
finally:
|
2021-07-07 16:57:09 +03:00
|
|
|
if self.listen_jsonrpc:
|
|
|
|
|
self.logger.info("removing lockfile")
|
|
|
|
|
remove_lockfile(get_lockfile(self.config))
|
2021-03-17 19:16:07 +01:00
|
|
|
self.logger.info("stopped")
|
2021-11-05 17:24:03 +01:00
|
|
|
self._stopped_event.set()
|
2016-01-31 11:43:11 +09:00
|
|
|
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
def run_gui(self) -> None:
|
|
|
|
|
assert self.config
|
2021-11-09 01:02:57 +01:00
|
|
|
threading.current_thread().name = 'GUI'
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
gui_name = self.config.GUI_NAME
|
2016-01-31 11:43:11 +09:00
|
|
|
if gui_name in ['lite', 'classic']:
|
|
|
|
|
gui_name = 'qt'
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
self._plugins = Plugins(self.config, gui_name) # init plugins
|
2019-12-10 23:31:58 +01:00
|
|
|
self.logger.info(f'launching GUI: {gui_name}')
|
2018-02-15 17:30:40 +01:00
|
|
|
try:
|
2022-04-11 16:53:25 +02:00
|
|
|
try:
|
|
|
|
|
gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])
|
|
|
|
|
except GuiImportError as e:
|
|
|
|
|
sys.exit(str(e))
|
run_electrum: have daemon manage Plugins object, and call Plugins.stop
Plugins.stop was never called, so the Plugins thread only stopped
because of the is_running() check in run(), which triggers too late:
the Plugins thread was stopping after the main thread stopped.
E.g. playing around in the qt wizard with wallet creation for a Trezor,
and closing the wizard (only window):
``` 24.85 | E | p/plugin.Plugins |
Traceback (most recent call last):
File "/home/user/wspace/electrum/electrum/util.py", line 386, in run_jobs
job.run()
File "/home/user/wspace/electrum/electrum/plugin.py", line 430, in run
client.timeout(cutoff)
File "/home/user/wspace/electrum/electrum/plugin.py", line 363, in wrapper
return run_in_hwd_thread(partial(func, *args, **kwargs))
File "/home/user/wspace/electrum/electrum/plugin.py", line 355, in run_in_hwd_thread
fut = _hwd_comms_executor.submit(func)
File "/usr/lib/python3.10/concurrent/futures/thread.py", line 167, in submit
raise RuntimeError('cannot schedule new futures after shutdown')
RuntimeError: cannot schedule new futures after shutdown
```
2023-08-24 16:56:23 +00:00
|
|
|
self.gui_object = gui.ElectrumGui(config=self.config, daemon=self, plugins=self._plugins)
|
2021-11-05 18:33:03 +01:00
|
|
|
if not self._stop_entered:
|
2021-11-05 17:24:03 +01:00
|
|
|
self.gui_object.main()
|
|
|
|
|
else:
|
|
|
|
|
# If daemon.stop() was called before gui_object got created, stop gui now.
|
|
|
|
|
self.gui_object.stop()
|
2018-02-15 17:30:40 +01:00
|
|
|
except BaseException as e:
|
2020-02-19 15:45:36 +01:00
|
|
|
self.logger.error(f'GUI raised exception: {repr(e)}. shutting down.')
|
|
|
|
|
raise
|
|
|
|
|
finally:
|
2018-02-15 17:30:40 +01:00
|
|
|
# app will exit now
|
2021-11-05 17:24:03 +01:00
|
|
|
asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result()
|
2022-07-06 18:33:08 +02:00
|
|
|
|
|
|
|
|
@with_wallet_lock
|
|
|
|
|
def _check_password_for_directory(self, *, old_password, new_password=None, wallet_dir: str) -> Tuple[bool, bool]:
|
|
|
|
|
"""Checks password against all wallets (in dir), returns whether they can be unified and whether they are already.
|
|
|
|
|
If new_password is not None, update all wallet passwords to new_password.
|
|
|
|
|
"""
|
|
|
|
|
assert os.path.exists(wallet_dir), f"path {wallet_dir!r} does not exist"
|
|
|
|
|
failed = []
|
|
|
|
|
is_unified = True
|
|
|
|
|
for filename in os.listdir(wallet_dir):
|
|
|
|
|
path = os.path.join(wallet_dir, filename)
|
|
|
|
|
path = standardize_path(path)
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
continue
|
|
|
|
|
wallet = self.get_wallet(path)
|
2023-03-20 19:54:53 +00:00
|
|
|
# note: we only create a new wallet object if one was not loaded into the wallet already.
|
|
|
|
|
# This is to avoid having two wallet objects contending for the same file.
|
|
|
|
|
# Take care: this only works if the daemon knows about all wallet objects.
|
|
|
|
|
# if other code already has created a Wallet() for a file but did not tell the daemon,
|
|
|
|
|
# hard-to-understand bugs will follow...
|
2022-07-06 18:33:08 +02:00
|
|
|
if wallet is None:
|
|
|
|
|
try:
|
2023-09-22 11:49:53 +02:00
|
|
|
wallet = self._load_wallet(path, old_password, upgrade=True, config=self.config)
|
2022-07-06 18:33:08 +02:00
|
|
|
except util.InvalidPassword:
|
|
|
|
|
pass
|
|
|
|
|
except Exception:
|
|
|
|
|
self.logger.exception(f'failed to load wallet at {path!r}:')
|
|
|
|
|
if wallet is None:
|
|
|
|
|
failed.append(path)
|
|
|
|
|
continue
|
|
|
|
|
if not wallet.storage.is_encrypted():
|
|
|
|
|
is_unified = False
|
|
|
|
|
try:
|
2023-03-20 20:00:28 +00:00
|
|
|
try:
|
|
|
|
|
wallet.check_password(old_password)
|
|
|
|
|
old_password_real = old_password
|
|
|
|
|
except util.InvalidPassword:
|
|
|
|
|
wallet.check_password(None)
|
|
|
|
|
old_password_real = None
|
2022-07-06 18:33:08 +02:00
|
|
|
except Exception:
|
|
|
|
|
failed.append(path)
|
|
|
|
|
continue
|
|
|
|
|
if new_password:
|
|
|
|
|
self.logger.info(f'updating password for wallet: {path!r}')
|
2023-03-20 20:00:28 +00:00
|
|
|
wallet.update_password(old_password_real, new_password, encrypt_storage=True)
|
2022-07-06 18:33:08 +02:00
|
|
|
can_be_unified = failed == []
|
|
|
|
|
is_unified = can_be_unified and is_unified
|
|
|
|
|
return can_be_unified, is_unified
|
|
|
|
|
|
|
|
|
|
@with_wallet_lock
|
|
|
|
|
def update_password_for_directory(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
old_password,
|
|
|
|
|
new_password,
|
|
|
|
|
wallet_dir: Optional[str] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""returns whether password is unified"""
|
|
|
|
|
if new_password is None:
|
|
|
|
|
# we opened a non-encrypted wallet
|
|
|
|
|
return False
|
|
|
|
|
if wallet_dir is None:
|
|
|
|
|
wallet_dir = os.path.dirname(self.config.get_wallet_path())
|
|
|
|
|
can_be_unified, is_unified = self._check_password_for_directory(
|
|
|
|
|
old_password=old_password, new_password=None, wallet_dir=wallet_dir)
|
|
|
|
|
if not can_be_unified:
|
|
|
|
|
return False
|
|
|
|
|
if is_unified and old_password == new_password:
|
|
|
|
|
return True
|
|
|
|
|
self._check_password_for_directory(
|
|
|
|
|
old_password=old_password, new_password=new_password, wallet_dir=wallet_dir)
|
|
|
|
|
return True
|
2024-10-13 11:06:14 +02:00
|
|
|
|
|
|
|
|
def update_recently_opened_wallets(self, wallet_path, *, remove: bool = False):
|
|
|
|
|
recent = self.config.RECENTLY_OPEN_WALLET_FILES or []
|
|
|
|
|
if wallet_path in recent:
|
|
|
|
|
recent.remove(wallet_path)
|
|
|
|
|
if not remove:
|
|
|
|
|
recent.insert(0, wallet_path)
|
|
|
|
|
recent = [path for path in recent if os.path.exists(path)]
|
|
|
|
|
recent = recent[:5]
|
|
|
|
|
self.config.RECENTLY_OPEN_WALLET_FILES = recent
|
|
|
|
|
util.trigger_callback('recently_opened_wallets_update')
|