class Daemon(Logger): network: Optional[Network] @profiler def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): Logger.__init__(self) self.running = False self.running_lock = threading.Lock() self.config = config if fd is None and listen_jsonrpc: fd = get_file_descriptor(config) if fd is None: raise Exception('failed to lock daemon; already running?') self.asyncio_loop = asyncio.get_event_loop() self.network = None if not config.get('offline'): self.network = Network(config, daemon=self) self.fx = FxThread(config, self.network) self.gui_object = None # path -> wallet; make sure path is standardized. self._wallets = {} # type: Dict[str, Abstract_Wallet] daemon_jobs = [] # Setup commands server self.commands_server = None if listen_jsonrpc: self.commands_server = CommandsServer(self, fd) daemon_jobs.append(self.commands_server.run()) # pay server self.pay_server = None payserver_address = self.config.get_netaddress('payserver_address') if not config.get('offline') and payserver_address: self.pay_server = PayServer(self, payserver_address) daemon_jobs.append(self.pay_server.run()) # server-side watchtower self.watchtower = None watchtower_address = self.config.get_netaddress('watchtower_address') if not config.get('offline') and watchtower_address: self.watchtower = WatchTowerServer(self.network, watchtower_address) daemon_jobs.append(self.watchtower.run) if self.network: self.network.start(jobs=[self.fx.run]) self.taskgroup = TaskGroup() asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) @log_exceptions async def _run(self, jobs: Iterable = None): if jobs is None: jobs = [] self.logger.info("starting taskgroup.") try: async with self.taskgroup as group: [await group.spawn(job) for job in jobs] await group.spawn(asyncio.Event().wait ) # run forever (until cancel) except asyncio.CancelledError: raise except Exception as e: self.logger.exception("taskgroup died.") finally: self.logger.info("taskgroup stopped.") def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]: path = standardize_path(path) # wizard will be launched if we return if path in self._wallets: wallet = self._wallets[path] return wallet storage = WalletStorage(path) if not storage.file_exists(): return if storage.is_encrypted(): if not password: return storage.decrypt(password) # read data, pass it to db db = WalletDB(storage.read(), manual_upgrades=manual_upgrades) if db.requires_split(): return if db.requires_upgrade(): return if db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.network) self._wallets[path] = wallet return wallet def add_wallet(self, wallet: Abstract_Wallet) -> None: path = wallet.storage.path path = standardize_path(path) self._wallets[path] = wallet def get_wallet(self, path: str) -> Optional[Abstract_Wallet]: path = standardize_path(path) return self._wallets.get(path) def get_wallets(self) -> Dict[str, Abstract_Wallet]: return dict(self._wallets) # copy def delete_wallet(self, path: str) -> bool: self.stop_wallet(path) if os.path.exists(path): os.unlink(path) return True return False def stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" path = standardize_path(path) wallet = self._wallets.pop(path, None) if not wallet: return False wallet.stop() return True def run_daemon(self): self.running = True try: while self.is_running(): time.sleep(0.1) except KeyboardInterrupt: self.running = False self.on_stop() def is_running(self): with self.running_lock: return self.running and not self.taskgroup.closed() def stop(self): with self.running_lock: self.running = False def on_stop(self): if self.gui_object: self.gui_object.stop() # stop network/wallets for k, wallet in self._wallets.items(): wallet.stop() if self.network: self.logger.info("shutting down network") self.network.stop() self.logger.info("stopping taskgroup") fut = asyncio.run_coroutine_threadsafe( self.taskgroup.cancel_remaining(), self.asyncio_loop) try: fut.result(timeout=2) except (concurrent.futures.TimeoutError, concurrent.futures.CancelledError, asyncio.CancelledError): pass self.logger.info("removing lockfile") remove_lockfile(get_lockfile(self.config)) self.logger.info("stopped") def run_gui(self, config, plugins): threading.current_thread().setName('GUI') gui_name = config.get('gui', 'qt') if gui_name in ['lite', 'classic']: gui_name = 'qt' self.logger.info(f'launching GUI: {gui_name}') try: gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) self.gui_object = gui.ElectrumGui(config, self, plugins) self.gui_object.main() except BaseException as e: self.logger.error( f'GUI raised exception: {repr(e)}. shutting down.') raise finally: # app will exit now self.on_stop()
class Daemon(Logger): @profiler def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): Logger.__init__(self) self.auth_lock = asyncio.Lock() self.running = False self.running_lock = threading.Lock() self.config = config if fd is None and listen_jsonrpc: fd = get_file_descriptor(config) if fd is None: raise Exception('failed to lock daemon; already running?') self.asyncio_loop = asyncio.get_event_loop() self.network = None if not config.get('offline'): self.network = Network(config, daemon=self) self.fx = FxThread(config, self.network) self.gui_object = None # path -> wallet; make sure path is standardized. self._wallets = {} # type: Dict[str, Abstract_Wallet] daemon_jobs = [] # Setup JSONRPC server if listen_jsonrpc: daemon_jobs.append(self.start_jsonrpc(config, fd)) # request server self.pay_server = None if not config.get('offline') and self.config.get('run_payserver'): self.pay_server = PayServer(self) daemon_jobs.append(self.pay_server.run()) # server-side watchtower self.watchtower = None if not config.get('offline') and self.config.get('run_watchtower'): self.watchtower = WatchTowerServer(self.network) daemon_jobs.append(self.watchtower.run) if self.network: self.network.start(jobs=[self.fx.run]) self.taskgroup = TaskGroup() asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) @log_exceptions async def _run(self, jobs: Iterable = None): if jobs is None: jobs = [] try: async with self.taskgroup as group: [await group.spawn(job) for job in jobs] await group.spawn(asyncio.Event().wait ) # run forever (until cancel) except BaseException as e: self.logger.exception('daemon.taskgroup died.') finally: self.logger.info("stopping daemon.taskgroup") 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) request = await request.text() response = await jsonrpcserver.async_dispatch(request, methods=self.methods) if isinstance(response, jsonrpcserver.response.ExceptionResponse): self.logger.error(f"error handling request: {request}", exc_info=response.exc) # this exposes the error message to the client response.message = str(response.exc) if response.wanted: return web.json_response(response.deserialized(), status=response.http_status) else: return web.Response() async def start_jsonrpc(self, config: SimpleConfig, fd): self.app = web.Application() self.app.router.add_post("/", self.handle) self.rpc_user, self.rpc_password = get_rpc_credentials(config) self.methods = jsonrpcserver.methods.Methods() self.methods.add(self.ping) self.methods.add(self.gui) self.cmd_runner = Commands(config=self.config, network=self.network, daemon=self) for cmdname in known_commands: self.methods.add(getattr(self.cmd_runner, cmdname)) self.methods.add(self.run_cmdline) self.host = config.get('rpchost', '127.0.0.1') self.port = config.get('rpcport', 0) self.runner = web.AppRunner(self.app) await self.runner.setup() site = web.TCPSite(self.runner, self.host, self.port) await site.start() socket = site._server.sockets[0] os.write(fd, bytes(repr((socket.getsockname(), time.time())), 'utf8')) os.close(fd) async def ping(self): return True async def gui(self, config_options): if self.gui_object: if hasattr(self.gui_object, 'new_window'): path = self.config.get_wallet_path(use_gui_last_wallet=True) self.gui_object.new_window(path, config_options.get('url')) response = "ok" else: response = "error: current GUI does not support multiple windows" else: response = "Error: Electrum is running in daemon mode. Please stop the daemon first." return response def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]: path = standardize_path(path) # wizard will be launched if we return if path in self._wallets: wallet = self._wallets[path] return wallet storage = WalletStorage(path) if not storage.file_exists(): return if storage.is_encrypted(): if not password: return storage.decrypt(password) # read data, pass it to db db = WalletDB(storage.read(), manual_upgrades=manual_upgrades) if db.requires_split(): return if db.requires_upgrade(): return if db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.network) self._wallets[path] = wallet self.wallet = wallet return wallet def add_wallet(self, wallet: Abstract_Wallet) -> None: path = wallet.storage.path path = standardize_path(path) self._wallets[path] = wallet def get_wallet(self, path: str) -> Abstract_Wallet: path = standardize_path(path) return self._wallets.get(path) def get_wallets(self) -> Dict[str, Abstract_Wallet]: return dict(self._wallets) # copy def delete_wallet(self, path: str) -> bool: self.stop_wallet(path) if os.path.exists(path): os.unlink(path) return True return False def stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" path = standardize_path(path) wallet = self._wallets.pop(path, None) if not wallet: return False wallet.stop_threads() return True async def run_cmdline(self, config_options): cmdname = config_options['cmd'] cmd = known_commands[cmdname] # 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) if cmd.requires_wallet: kwargs['wallet_path'] = config_options.get('wallet_path') func = getattr(self.cmd_runner, cmd.name) # fixme: not sure how to retrieve message in jsonrpcclient try: result = await func(*args, **kwargs) except Exception as e: result = {'error': str(e)} return result def run_daemon(self): self.running = True try: while self.is_running(): time.sleep(0.1) except KeyboardInterrupt: self.running = False self.on_stop() def is_running(self): with self.running_lock: return self.running and not self.taskgroup.closed() def stop(self): with self.running_lock: self.running = False def on_stop(self): if self.gui_object: self.gui_object.stop() # stop network/wallets for k, wallet in self._wallets.items(): wallet.stop_threads() if self.network: self.logger.info("shutting down network") self.network.stop() self.logger.info("stopping taskgroup") fut = asyncio.run_coroutine_threadsafe( self.taskgroup.cancel_remaining(), self.asyncio_loop) try: fut.result(timeout=2) except (asyncio.TimeoutError, asyncio.CancelledError): pass self.logger.info("removing lockfile") remove_lockfile(get_lockfile(self.config)) self.logger.info("stopped") def run_gui(self, config, plugins): threading.current_thread().setName('GUI') gui_name = config.get('gui', 'qt') if gui_name in ['lite', 'classic']: gui_name = 'qt' self.logger.info(f'launching GUI: {gui_name}') try: gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) self.gui_object = gui.ElectrumGui(config, self, plugins) self.gui_object.main() except BaseException as e: self.logger.error( f'GUI raised exception: {repr(e)}. shutting down.') raise finally: # app will exit now self.on_stop()