diff --git a/electrum/commands.py b/electrum/commands.py index 344982e54..de1313dd6 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -217,7 +217,6 @@ class Commands: @command('n') async def stop(self): """Stop daemon""" - # TODO it would be nice if this could stop the GUI too await self.daemon.stop() return "Daemon stopped" diff --git a/electrum/daemon.py b/electrum/daemon.py index 2ebe236d2..211d197b6 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -487,8 +487,8 @@ class Daemon(Logger): if self.config.get('use_gossip', False): self.network.start_gossip() - self.stopping_soon = threading.Event() - self.stopped_event = asyncio.Event() + self._stopping_soon = threading.Event() + self._stopped_event = threading.Event() self.taskgroup = TaskGroup() asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) @@ -507,7 +507,7 @@ class Daemon(Logger): self.logger.exception("taskgroup died.") finally: self.logger.info("taskgroup stopped.") - self.stopping_soon.set() + await self.stop() def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]: path = standardize_path(path) @@ -571,41 +571,35 @@ class Daemon(Logger): def run_daemon(self): try: - self.stopping_soon.wait() + self._stopping_soon.wait() except KeyboardInterrupt: - self.stopping_soon.set() - self.on_stop() + asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result() + self._stopped_event.wait() async def stop(self): - self.stopping_soon.set() - await self.stopped_event.wait() - - def on_stop(self): + if self._stopping_soon.is_set(): + return + self._stopping_soon.set() + self.logger.info("stop() entered. initiating shutdown") try: - self.logger.info("on_stop() entered. initiating shutdown") if self.gui_object: self.gui_object.stop() - - async def stop_async(): - self.logger.info("stopping all wallets") + self.logger.info("stopping all wallets") + async with TaskGroup() as group: + for k, wallet in self._wallets.items(): + await group.spawn(wallet.stop()) + self.logger.info("stopping network and taskgroup") + async with ignore_after(2): async with TaskGroup() as group: - for k, wallet in self._wallets.items(): - await group.spawn(wallet.stop()) - self.logger.info("stopping network and taskgroup") - async with ignore_after(2): - async with TaskGroup() as group: - if self.network: - await group.spawn(self.network.stop(full_shutdown=True)) - await group.spawn(self.taskgroup.cancel_remaining()) - - fut = asyncio.run_coroutine_threadsafe(stop_async(), self.asyncio_loop) - fut.result() + if self.network: + await group.spawn(self.network.stop(full_shutdown=True)) + await group.spawn(self.taskgroup.cancel_remaining()) finally: if self.listen_jsonrpc: self.logger.info("removing lockfile") remove_lockfile(get_lockfile(self.config)) self.logger.info("stopped") - self.asyncio_loop.call_soon_threadsafe(self.stopped_event.set) + self._stopped_event.set() def run_gui(self, config, plugins): threading.current_thread().setName('GUI') @@ -616,10 +610,14 @@ class Daemon(Logger): try: gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) self.gui_object = gui.ElectrumGui(config, self, plugins) - self.gui_object.main() + if not self._stopping_soon.is_set(): + self.gui_object.main() + else: + # If daemon.stop() was called before gui_object got created, stop gui now. + self.gui_object.stop() except BaseException as e: self.logger.error(f'GUI raised exception: {repr(e)}. shutting down.') raise finally: # app will exit now - self.on_stop() + asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result() diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 7f6658e16..80b53187e 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -81,6 +81,7 @@ class OpenFileEventFilter(QObject): class QElectrumApplication(QApplication): new_window_signal = pyqtSignal(str, object) + quit_signal = pyqtSignal() class QNetworkUpdatedSignalObject(QObject): @@ -132,6 +133,7 @@ class ElectrumGui(Logger): self.tray = None self._init_tray() self.app.new_window_signal.connect(self.start_new_window) + self.app.quit_signal.connect(self.app.quit, Qt.QueuedConnection) self.set_dark_theme_if_needed() run_hook('init_qt', self) @@ -428,5 +430,8 @@ class ElectrumGui(Logger): # on some platforms the exec_ call may not return, so use _cleanup_before_exit def stop(self): + """Stops the GUI. + This method is thread-safe. + """ self.logger.info('closing GUI') - self.app.quit() + self.app.quit_signal.emit()