From 1cce216c1f44149010acf31f9d36eb2c010689af Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 28 Apr 2025 16:55:21 +0200 Subject: [PATCH 1/3] fix: prevent PluginsDialog from getting in bad state If the plugin file got already deleted while being in the installation dialog, trying to delete it again will raise an exception. This is fixed by catching the exception. If the user tries to install an external plugin that is already installed, and then closes the PluginDialog, the PluginsDialog will get into a bad state, throwing an exeption when opening it. This happens because the add_plugin_dialog deletes the zipfile if the user closes or cancels the installation dialog. This is fixed by checking if the plugin is already existing, instead of trying to install an already existing plugin. --- electrum/gui/qt/plugins_dialog.py | 17 ++++++++++++++--- electrum/plugin.py | 4 +++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 7b1e3a52f..07d1124e7 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -7,6 +7,7 @@ from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QWidg from PyQt6.QtCore import Qt from electrum.i18n import _ +from electrum.logging import get_logger from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin, EnterButton from .util import read_QIcon_from_bytes, IconLabel @@ -88,7 +89,7 @@ class PluginDialog(WindowModalDialog): _('Settings'), partial(p.settings_dialog, self)) buttons.insert(1, settings_button) - # add buttonss + # add buttons vbox.addLayout(Buttons(*buttons)) def do_toggle(self): @@ -150,6 +151,7 @@ class PluginStatusButton(QPushButton): class PluginsDialog(WindowModalDialog, MessageBoxMixin): + _logger = get_logger(__name__) def __init__(self, config: 'SimpleConfig', plugins:'Plugins', *, gui_object: Optional['ElectrumGui'] = None): WindowModalDialog.__init__(self, None, _('Electrum Plugins')) @@ -264,7 +266,10 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): self.show_error(f"{e}") success = False if not success: - os.unlink(path) + try: + os.unlink(path) + except FileNotFoundError: + self._logger.debug("", exc_info=True) def add_plugin_dialog(self): pubkey, salt = self.plugins.get_pubkey_bytes() @@ -276,6 +281,9 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): return plugins_dir = self.plugins.get_external_plugin_dir() path = os.path.join(plugins_dir, os.path.basename(filename)) + if os.path.exists(path): + self.show_warning(_('Plugin already installed.')) + return shutil.copyfile(filename, path) try: success = self.add_external_plugin(path) @@ -283,7 +291,10 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): self.show_error(f"{e}") success = False if not success: - os.unlink(path) + try: + os.unlink(path) + except FileNotFoundError: + self._logger.debug("", exc_info=True) def add_external_plugin(self, path): manifest = self.plugins.read_manifest(path) diff --git a/electrum/plugin.py b/electrum/plugin.py index c2e5cd7dd..068125f22 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -246,10 +246,12 @@ class Plugins(DaemonThread): os.mkdir(pkg_path) return pkg_path - async def download_external_plugin(self, url): + async def download_external_plugin(self, url: str) -> str: filename = os.path.basename(urlparse(url).path) pkg_path = self.get_external_plugin_dir() path = os.path.join(pkg_path, filename) + if os.path.exists(path): + raise FileExistsError(f"Plugin {filename} already exists at {path}") async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status == 200: From eed944368c89a59b09a630ccc1d574f81c4da8c5 Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 29 Apr 2025 11:01:07 +0200 Subject: [PATCH 2/3] fix: download_plugin_dialog has no access to run_coroutine_dialog download_plugin_dialog tried to run the zip download with self.window.run_coroutine_dialog, but self has no window, instanciating a RunCoroutineDialog directly fixes this. --- electrum/gui/qt/plugins_dialog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 07d1124e7..87f756505 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -9,8 +9,8 @@ from PyQt6.QtCore import Qt from electrum.i18n import _ from electrum.logging import get_logger -from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin, EnterButton -from .util import read_QIcon_from_bytes, IconLabel +from .util import (WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin, + EnterButton, read_QIcon_from_bytes, IconLabel, RunCoroutineDialog) if TYPE_CHECKING: @@ -254,7 +254,8 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): return coro = self.plugins.download_external_plugin(url) try: - path = self.window.run_coroutine_dialog(coro, _("Downloading plugin...")) + d = RunCoroutineDialog(self, _("Downloading plugin..."), coro) + path = d.run() except UserCancelled: return except Exception as e: From 84632f53d2a9d04ac7af885e8ba00e28619a8a2d Mon Sep 17 00:00:00 2001 From: f321x Date: Tue, 29 Apr 2025 11:21:38 +0200 Subject: [PATCH 3/3] fix: bring dialog to front after reloading When reloading the windows, e.g. after enabling or disabling a plugin, the plugins dialog vanished behind the main window. By using a Timer its possible to bring the Dialog back in front after the windows have reloaded. --- electrum/gui/qt/plugins_dialog.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 87f756505..7bda79cb4 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -4,7 +4,7 @@ import shutil import os from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QWidget, QScrollArea, QFormLayout, QFileDialog, QMenu, QApplication -from PyQt6.QtCore import Qt +from PyQt6.QtCore import QTimer from electrum.i18n import _ from electrum.logging import get_logger @@ -365,8 +365,7 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): status_button.update() if self.gui_object: self.gui_object.reload_windows() - self.setFocus() - self.activateWindow() + self.bring_to_front() def uninstall_plugin(self, name): if not self.question(_('Remove plugin \'{}\'?').format(name)): @@ -375,3 +374,10 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): if self.gui_object: self.gui_object.reload_windows() self.show_list() + self.bring_to_front() + + def bring_to_front(self): + def _bring_self_to_front(): + self.activateWindow() + self.setFocus() + QTimer.singleShot(100, _bring_self_to_front)