Exemple #1
0
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()
Exemple #2
0
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()