From cb39737a39cd80ac4e8d26f3e1374cbb4c9ed847 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Mar 2025 11:40:26 +0100 Subject: [PATCH] Plugins call with cmd_only: - pass temporary config to Plugins - load only enabled plugins - parse the command line again after plugins are loaded --- electrum/commands.py | 13 +------------ electrum/plugin.py | 18 +++++++----------- run_electrum | 25 ++++++++++++++----------- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 8a59f425c..fea336408 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1594,18 +1594,6 @@ def plugin_command(s, plugin_name = None): return func_wrapper return decorator -def remove_disabled_plugin_commands(config: SimpleConfig, plugins_with_commands: set[str]): - """Removes registered commands of plugins that are not enabled in the config.""" - global known_commands - global plugin_commands - assert len(known_commands) > 0, "known_commands should not be empty, called too early?" - for plugin_name in plugins_with_commands: - if not config.get(f'enable_plugin_{plugin_name}'): - registered_commands: set[str] = plugin_commands[plugin_name] - assert len(registered_commands) > 0, "plugin command registered with the invalid plugin name" - for command_name in registered_commands: - del known_commands[command_name] - delattr(Commands, command_name) def eval_bool(x: str) -> bool: if x == 'false': return False @@ -1890,5 +1878,6 @@ def get_parser(): group.add_argument(k, nargs='?', help=v) # 'gui' is the default command + # note: set_default_subparser modifies sys.argv parser.set_default_subparser('gui') return parser diff --git a/electrum/plugin.py b/electrum/plugin.py index 059f41f8e..ca99ed530 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -65,19 +65,20 @@ class Plugins(DaemonThread): pkgpath = os.path.dirname(plugins.__file__) @profiler - def __init__(self, config: SimpleConfig = None, gui_name = None, cmd_only: bool = False): + def __init__(self, config: SimpleConfig, gui_name = None, cmd_only: bool = False): + self.config = config self.cmd_only = cmd_only # type: bool self.internal_plugin_metadata = {} self.external_plugin_metadata = {} self.loaded_command_modules = set() # type: set[str] if cmd_only: # only import the command modules of plugins + Logger.__init__(self) self.find_plugins() return DaemonThread.__init__(self) self.device_manager = DeviceMgr(config) self.name = 'Plugins' # set name of thread - self.config = config self.hw_wallets = {} self.plugins = {} # type: Dict[str, BasePlugin] self.gui_name = gui_name @@ -86,15 +87,6 @@ class Plugins(DaemonThread): self.add_jobs(self.device_manager.thread_jobs()) self.start() - def __getattr__(self, item): - # to prevent accessing of a cmd_only instance of this class - if self.cmd_only: - if item == 'logger': - # if something tries to access logger and it is not initialized it gets initialized here - Logger.__init__(self) - return self.logger - raise Exception(f"This instance of Plugins is only for command importing, cannot access {item}") - @property def descriptions(self): return dict(list(self.internal_plugin_metadata.items()) + list(self.external_plugin_metadata.items())) @@ -108,6 +100,8 @@ class Plugins(DaemonThread): # we exclude the ones packaged as *code*, here: if loader.__class__.__qualname__ == "PyiFrozenImporter": continue + if self.cmd_only and self.config.get('enable_plugin_' + name) is not True: + continue full_name = f'electrum.plugins.{name}' + ('.commands' if self.cmd_only else '') spec = importlib.util.find_spec(full_name) if spec is None: @@ -233,6 +227,8 @@ class Plugins(DaemonThread): spec = zipfile.find_spec(name) module = self.exec_module_from_spec(spec, module_path) if self.cmd_only: + if self.config.get('enable_plugin_' + name) is not True: + continue spec2 = importlib.util.find_spec(module_path + '.commands') if spec2 is None: # no commands module in this plugin continue diff --git a/run_electrum b/run_electrum index 5771674a6..0b71a496f 100755 --- a/run_electrum +++ b/run_electrum @@ -103,7 +103,7 @@ from electrum.storage import WalletStorage from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled from electrum.util import InvalidPassword from electrum.plugin import Plugins -from electrum.commands import get_parser, known_commands, Commands, config_variables, remove_disabled_plugin_commands +from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum import daemon from electrum import keystore from electrum.util import create_and_start_event_loop, UserFacingException, JsonRPCError @@ -340,8 +340,6 @@ def main(): sys.argv[i] = input("Enter argument:") elif arg == ':': sys.argv[i] = prompt_password('Enter argument (will not echo):', confirm=False) - # load (only) the commands modules of plugins so their commands are registered - plugin_commands = Plugins(cmd_only=True) # config is an object passed to the various constructors (wallet, interface, gui) if is_android: @@ -360,15 +358,23 @@ def main(): # ~hack for easier regtest builds. pkgname subject to change. config_options['regtest'] = True else: + # save sys args for next parser + saved_sys_argv = sys.argv[:] + # disable help, the next parser will display it + if '-h' in sys.argv: + sys.argv.remove('-h') + # parse first without plugins + config_options = parse_command_line() + tmp_config = SimpleConfig(config_options) + # load (only) the commands modules of plugins so their commands are registered + plugin_commands = Plugins(tmp_config, cmd_only=True) + # re-parse command line + sys.argv = saved_sys_argv[:] config_options = parse_command_line() config = SimpleConfig(config_options) cmdname = config.get('cmd') - # now that we know which plugins are enabled we can remove commands of disabled plugins again - # enabled plugins depend on the datadir which is parsed by argparse, so they can only be unloaded afterwards - remove_disabled_plugin_commands(config, plugin_commands.loaded_command_modules) - # set language as early as possible # Note: we are already too late for strings that are declared in the global scope # of an already imported module. However, the GUI and the plugins at least have @@ -500,10 +506,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): else: # command line configure_logging(config, log_to_file=False) # don't spam logfiles for each client-side RPC, but support "-v" - cmd = known_commands.get(cmdname) - if not cmd: - print_stderr("unknown command:", cmdname) - sys_exit(1) + cmd = known_commands[cmdname] wallet_path = config.get_wallet_path() if not config.NETWORK_OFFLINE: init_cmdline(config_options, wallet_path, rpcserver=True, config=config)