Allow external plugins
- borrows code brom ElectronCash - external plugins are imported as zip files - check hash from plugins.json file
This commit is contained in:
@@ -150,7 +150,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self.reload_app_stylesheet()
|
||||
|
||||
# always load 2fa
|
||||
self.plugins.load_plugin('trustedcoin')
|
||||
self.plugins.load_internal_plugin('trustedcoin')
|
||||
|
||||
run_hook('init_qt', self)
|
||||
|
||||
|
||||
@@ -63,15 +63,15 @@ class PluginsDialog(WindowModalDialog):
|
||||
run_hook('init_qt', self.window.gui_object)
|
||||
|
||||
def show_list(self):
|
||||
descriptions = self.plugins.descriptions.values()
|
||||
for i, descr in enumerate(descriptions):
|
||||
full_name = descr['__name__']
|
||||
prefix, _separator, name = full_name.rpartition('.')
|
||||
descriptions = sorted(self.plugins.descriptions.items())
|
||||
i = 0
|
||||
for name, descr in descriptions:
|
||||
i += 1
|
||||
p = self.plugins.get(name)
|
||||
if descr.get('registers_keystore'):
|
||||
continue
|
||||
try:
|
||||
cb = QCheckBox(descr['fullname'])
|
||||
cb = QCheckBox(descr['display_name'])
|
||||
plugin_is_loaded = p is not None
|
||||
cb_enabled = (not plugin_is_loaded and self.plugins.is_available(name, self.wallet)
|
||||
or plugin_is_loaded and p.can_user_disable())
|
||||
|
||||
@@ -27,12 +27,18 @@ import pkgutil
|
||||
import importlib.util
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
import sys
|
||||
import json
|
||||
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
|
||||
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping, Set)
|
||||
import concurrent
|
||||
import zipimport
|
||||
from concurrent import futures
|
||||
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 .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException)
|
||||
@@ -53,6 +59,7 @@ hook_names = set()
|
||||
hooks = {}
|
||||
|
||||
|
||||
|
||||
class Plugins(DaemonThread):
|
||||
|
||||
LOGGING_SHORTCUT = 'p'
|
||||
@@ -66,16 +73,21 @@ class Plugins(DaemonThread):
|
||||
self.hw_wallets = {}
|
||||
self.plugins = {} # type: Dict[str, BasePlugin]
|
||||
self.internal_plugin_metadata = {}
|
||||
self.external_plugin_metadata = {}
|
||||
self.gui_name = gui_name
|
||||
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_external_plugins()
|
||||
self.load_plugins()
|
||||
self.add_jobs(self.device_manager.thread_jobs())
|
||||
self.start()
|
||||
|
||||
@property
|
||||
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]:
|
||||
"""Populates self.internal_plugin_metadata
|
||||
@@ -122,15 +134,88 @@ class Plugins(DaemonThread):
|
||||
|
||||
def load_plugins(self):
|
||||
self.load_internal_plugins()
|
||||
self.load_external_plugins()
|
||||
|
||||
def load_internal_plugins(self):
|
||||
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:
|
||||
self.load_plugin(name)
|
||||
self.load_internal_plugin(name)
|
||||
except BaseException as 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):
|
||||
return self.plugins.get(name)
|
||||
|
||||
@@ -141,9 +226,17 @@ class Plugins(DaemonThread):
|
||||
"""Imports the code of the given plugin.
|
||||
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:
|
||||
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)
|
||||
if spec is None:
|
||||
raise RuntimeError("%s implementation for %s plugin not found"
|
||||
|
||||
@@ -242,14 +242,15 @@ class SimpleConfig(Logger):
|
||||
self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
|
||||
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
|
||||
# Otherwise use the user's default data directory.
|
||||
path = self.get('electrum_path')
|
||||
if path is None:
|
||||
path = self.user_dir()
|
||||
|
||||
path = self.get('electrum_path') or self.user_dir()
|
||||
make_dir(path, allow_symlink=False)
|
||||
return path
|
||||
|
||||
def electrum_path(self):
|
||||
path = self.electrum_path_root()
|
||||
if self.get('testnet'):
|
||||
path = os.path.join(path, 'testnet')
|
||||
make_dir(path, allow_symlink=False)
|
||||
|
||||
Reference in New Issue
Block a user