Browse Source

Allow external plugins

- borrows code brom ElectronCash
 - external plugins are imported as zip files
 - check hash from plugins.json file
master
ThomasV 2 years ago
parent
commit
858d999d31
  1. 2
      electrum/gui/qt/__init__.py
  2. 10
      electrum/gui/qt/plugins_dialog.py
  3. 101
      electrum/plugin.py
  4. 11
      electrum/simple_config.py

2
electrum/gui/qt/__init__.py

@ -150,7 +150,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
self.reload_app_stylesheet() self.reload_app_stylesheet()
# always load 2fa # always load 2fa
self.plugins.load_plugin('trustedcoin') self.plugins.load_internal_plugin('trustedcoin')
run_hook('init_qt', self) run_hook('init_qt', self)

10
electrum/gui/qt/plugins_dialog.py

@ -63,15 +63,15 @@ class PluginsDialog(WindowModalDialog):
run_hook('init_qt', self.window.gui_object) run_hook('init_qt', self.window.gui_object)
def show_list(self): def show_list(self):
descriptions = self.plugins.descriptions.values() descriptions = sorted(self.plugins.descriptions.items())
for i, descr in enumerate(descriptions): i = 0
full_name = descr['__name__'] for name, descr in descriptions:
prefix, _separator, name = full_name.rpartition('.') i += 1
p = self.plugins.get(name) p = self.plugins.get(name)
if descr.get('registers_keystore'): if descr.get('registers_keystore'):
continue continue
try: try:
cb = QCheckBox(descr['fullname']) cb = QCheckBox(descr['display_name'])
plugin_is_loaded = p is not None plugin_is_loaded = p is not None
cb_enabled = (not plugin_is_loaded and self.plugins.is_available(name, self.wallet) cb_enabled = (not plugin_is_loaded and self.plugins.is_available(name, self.wallet)
or plugin_is_loaded and p.can_user_disable()) or plugin_is_loaded and p.can_user_disable())

101
electrum/plugin.py

@ -27,12 +27,18 @@ import pkgutil
import importlib.util import importlib.util
import time import time
import threading import threading
import traceback
import sys import sys
import json
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping, Set) Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping, Set)
import concurrent import concurrent
import zipimport
from concurrent import futures from concurrent import futures
from functools import wraps, partial from functools import wraps, partial
from enum import IntEnum
from packaging.version import parse as parse_version
from electrum.version import ELECTRUM_VERSION
from .i18n import _ from .i18n import _
from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
@ -53,6 +59,7 @@ hook_names = set()
hooks = {} hooks = {}
class Plugins(DaemonThread): class Plugins(DaemonThread):
LOGGING_SHORTCUT = 'p' LOGGING_SHORTCUT = 'p'
@ -66,16 +73,21 @@ class Plugins(DaemonThread):
self.hw_wallets = {} self.hw_wallets = {}
self.plugins = {} # type: Dict[str, BasePlugin] self.plugins = {} # type: Dict[str, BasePlugin]
self.internal_plugin_metadata = {} self.internal_plugin_metadata = {}
self.external_plugin_metadata = {}
self.gui_name = gui_name self.gui_name = gui_name
self.device_manager = DeviceMgr(config) self.device_manager = DeviceMgr(config)
self.user_pkgpath = os.path.join(self.config.electrum_path_root(), 'plugins')
if not os.path.exists(self.user_pkgpath):
os.mkdir(self.user_pkgpath)
self.find_internal_plugins() self.find_internal_plugins()
self.find_external_plugins()
self.load_plugins() self.load_plugins()
self.add_jobs(self.device_manager.thread_jobs()) self.add_jobs(self.device_manager.thread_jobs())
self.start() self.start()
@property @property
def descriptions(self): def descriptions(self):
return dict(list(self.internal_plugin_metadata.items())) return dict(list(self.internal_plugin_metadata.items()) + list(self.external_plugin_metadata.items()))
def find_internal_plugins(self) -> Mapping[str, dict]: def find_internal_plugins(self) -> Mapping[str, dict]:
"""Populates self.internal_plugin_metadata """Populates self.internal_plugin_metadata
@ -122,15 +134,88 @@ class Plugins(DaemonThread):
def load_plugins(self): def load_plugins(self):
self.load_internal_plugins() self.load_internal_plugins()
self.load_external_plugins()
def load_internal_plugins(self): def load_internal_plugins(self):
for name, d in self.internal_plugin_metadata.items(): for name, d in self.internal_plugin_metadata.items():
if self.config.get('enable_plugin_' + name) is True: if not d.get('requires_wallet_type') and self.config.get('enable_plugin_' + name):
try: try:
self.load_plugin(name) self.load_internal_plugin(name)
except BaseException as e: except BaseException as e:
self.logger.exception(f"cannot initialize plugin {name}: {e}") self.logger.exception(f"cannot initialize plugin {name}: {e}")
def load_external_plugin(self, name):
if name in self.plugins:
return self.plugins[name]
# If we do not have the metadata, it was not detected by `load_external_plugins`
# on startup, or added by manual user installation after that point.
metadata = self.external_plugin_metadata.get(name, None)
if metadata is None:
self.logger.exception("attempted to load unknown external plugin %s" % name)
return
from .crypto import sha256
external_plugin_dir = self.get_external_plugin_dir()
plugin_file_path = os.path.join(external_plugin_dir, name + '.zip')
if not os.path.exists(plugin_file_path):
return
with open(plugin_file_path, 'rb') as f:
s = f.read()
if sha256(s).hex() != metadata['hash']:
self.logger.exception("wrong hash for plugin '%s'" % plugin_file_path)
return
try:
zipfile = zipimport.zipimporter(plugin_file_path)
except zipimport.ZipImportError:
self.logger.exception("unable to load zip plugin '%s'" % plugin_file_path)
return
try:
module = zipfile.load_module(name)
except zipimport.ZipImportError as e:
self.logger.exception(f"unable to load zip plugin '{plugin_file_path}' package '{name}'")
return
sys.modules['electrum_external_plugins.'+ name] = module
full_name = f'electrum_external_plugins.{name}.{self.gui_name}'
spec = importlib.util.find_spec(full_name)
if spec is None:
raise RuntimeError("%s implementation for %s plugin not found"
% (self.gui_name, name))
module = importlib.util.module_from_spec(spec)
self._register_module(spec, module)
if sys.version_info >= (3, 10):
spec.loader.exec_module(module)
else:
module = spec.loader.load_module(full_name)
plugin = module.Plugin(self, self.config, name)
self.add_jobs(plugin.thread_jobs())
self.plugins[name] = plugin
self.logger.info(f"loaded external plugin {name}")
return plugin
@staticmethod
def _register_module(spec, module):
# sys.modules needs to be modified for relative imports to work
# see https://stackoverflow.com/a/50395128
sys.modules[spec.name] = module
def get_external_plugin_dir(self):
return self.user_pkgpath
def find_external_plugins(self):
""" read json file """
from .constants import read_json
self.external_plugin_metadata = read_json('plugins.json', {})
def load_external_plugins(self):
for name, d in self.external_plugin_metadata.items():
if not d.get('requires_wallet_type') and self.config.get('use_' + name):
try:
self.load_external_plugin(name)
except BaseException as e:
traceback.print_exc(file=sys.stdout) # shouldn't this be... suppressed unless -v?
self.logger.exception(f"cannot initialize plugin {name} {e!r}")
def get(self, name): def get(self, name):
return self.plugins.get(name) return self.plugins.get(name)
@ -141,9 +226,17 @@ class Plugins(DaemonThread):
"""Imports the code of the given plugin. """Imports the code of the given plugin.
note: can be called from any thread. note: can be called from any thread.
""" """
if name in self.internal_plugin_metadata:
return self.load_internal_plugin(name)
elif name in self.external_plugin_metadata:
return self.load_external_plugin(name)
else:
raise Exception()
def load_internal_plugin(self, name) -> 'BasePlugin':
if name in self.plugins: if name in self.plugins:
return self.plugins[name] return self.plugins[name]
full_name = f'electrum.plugins.{name}.{self.gui_name}' full_name = f'electrum.plugins.{name}' + f'.{self.gui_name}'
spec = importlib.util.find_spec(full_name) spec = importlib.util.find_spec(full_name)
if spec is None: if spec is None:
raise RuntimeError("%s implementation for %s plugin not found" raise RuntimeError("%s implementation for %s plugin not found"

11
electrum/simple_config.py

@ -242,14 +242,15 @@ class SimpleConfig(Logger):
self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP
def electrum_path(self): def electrum_path_root(self):
# Read electrum_path from command line # Read electrum_path from command line
# Otherwise use the user's default data directory. # Otherwise use the user's default data directory.
path = self.get('electrum_path') path = self.get('electrum_path') or self.user_dir()
if path is None:
path = self.user_dir()
make_dir(path, allow_symlink=False) make_dir(path, allow_symlink=False)
return path
def electrum_path(self):
path = self.electrum_path_root()
if self.get('testnet'): if self.get('testnet'):
path = os.path.join(path, 'testnet') path = os.path.join(path, 'testnet')
make_dir(path, allow_symlink=False) make_dir(path, allow_symlink=False)

Loading…
Cancel
Save