diff --git a/pyinstaller/hooks/hook-cryptoadvance.specter.services.py b/pyinstaller/hooks/hook-cryptoadvance.specter.services.py index ad0cf411ed..6a0039a23c 100644 --- a/pyinstaller/hooks/hook-cryptoadvance.specter.services.py +++ b/pyinstaller/hooks/hook-cryptoadvance.specter.services.py @@ -1,20 +1,20 @@ import logging -from cryptoadvance.specter.managers.service_manager import ServiceManager +from cryptoadvance.specter.managers.service_manager import ExtensionManager logger = logging.getLogger(__name__) # Collecting template and static files from the different services in src/cryptoadvance/specter/services service_template_datas = [ (service_dir, "templates") - for service_dir in ServiceManager.get_service_x_dirs("templates") + for service_dir in ExtensionManager.get_service_x_dirs("templates") ] service_static_datas = [ (service_dir, "static") - for service_dir in ServiceManager.get_service_x_dirs("static") + for service_dir in ExtensionManager.get_service_x_dirs("static") ] # Collect Packages from the services, including service- and controller-classes -service_packages = ServiceManager.get_service_packages() +service_packages = ExtensionManager.get_service_packages() datas = [*service_template_datas, *service_static_datas] diff --git a/src/cryptoadvance/specter/managers/service_manager.py b/src/cryptoadvance/specter/managers/service_manager.py new file mode 100644 index 0000000000..2d09b83103 --- /dev/null +++ b/src/cryptoadvance/specter/managers/service_manager.py @@ -0,0 +1,437 @@ +import json +import logging +import os +from importlib import import_module +from inspect import isclass +from pathlib import Path +from pkgutil import iter_modules +import sys +from typing import Dict, List + +from cryptoadvance.specter.config import ProductionConfig +from cryptoadvance.specter.device import Device +from cryptoadvance.specter.specter_error import SpecterError +from cryptoadvance.specter.user import User +from cryptoadvance.specter.util.reflection import get_template_static_folder +from flask import current_app as app +from flask import url_for +from flask.blueprints import Blueprint + +from ..services.service import Service +from ..services import callbacks, ExtensionException +from ..services.service_encrypted_storage import ( + ServiceEncryptedStorageManager, + ServiceUnencryptedStorageManager, +) +from ..util.reflection import ( + _get_module_from_class, + get_classlist_of_type_clazz_from_modulelist, + get_package_dir_for_subclasses_of, + get_subclasses_for_clazz, + get_subclasses_for_clazz_in_cwd, +) +from ..util.reflection_fs import search_dirs_in_path + +logger = logging.getLogger(__name__) + + +class ExtensionManager: + """Loads support for all Services it auto-discovers.""" + + def __init__(self, specter, devstatus_threshold): + self.specter = specter + specter.ext = {} + self.devstatus_threshold = devstatus_threshold + + # Each Service class is stored here, keyed on its Service.id str + self._services: Dict[str, Service] = {} + logger.info("----> starting service discovery Static") + # How do we discover services? Two configs are relevant: + # * SERVICES_LOAD_FROM_CWD (boolean, CWD is current working directory) + # * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specterext.swan.service"]) + # Ensuring security (especially for the CWD) is NOT done here but + # in the corresponding (Production)Config + logger.debug(f"EXTENSION_LIST = {app.config.get('EXTENSION_LIST')}") + class_list = get_classlist_of_type_clazz_from_modulelist( + Service, app.config.get("EXTENSION_LIST", []) + ) + + if app.config.get("SERVICES_LOAD_FROM_CWD", False): + logger.info("----> starting service discovery dynamic") + class_list.extend(get_subclasses_for_clazz_in_cwd(Service)) + else: + logger.info("----> skipping service discovery dynamic") + logger.info("----> starting service loading") + class_list = set(class_list) # remove duplicates (shouldn't happen but ...) + for clazz in class_list: + compare_map = {"alpha": 1, "beta": 2, "prod": 3} + if compare_map[self.devstatus_threshold] <= compare_map[clazz.devstatus]: + logger.info(f"Loading Service {clazz.__name__} from {clazz.__module__}") + # First configure the service + self.configure_service_for_module(clazz) + # Now activate it + self._services[clazz.id] = clazz( + active=clazz.id in self.specter.config.get("services", []), + specter=self.specter, + ) + self.specter.ext[clazz.id] = self._services[clazz.id] + # maybe register the blueprint + self.register_blueprint_for_ext(clazz, self._services[clazz.id]) + self.add_devices_for_ext(clazz, self._services[clazz.id]) + logger.info(f"Service {clazz.__name__} activated ({clazz.devstatus})") + else: + logger.info( + f"Service {clazz.__name__} not activated due to devstatus ( {self.devstatus_threshold} > {clazz.devstatus} )" + ) + logger.info("----> finished service processing") + self.execute_ext_callbacks("afterExtensionManagerInit") + + @classmethod + def register_blueprint_for_ext(cls, clazz, ext): + if not clazz.has_blueprint: + return + if hasattr(clazz, "blueprint_modules"): + controller_modules = clazz.blueprint_modules + setattr(clazz, "blueprints", {}) + elif hasattr(clazz, "blueprint_module"): + controller_modules = {"default": clazz.blueprint_module} + else: + import_name = f"cryptoadvance.specter.services.{clazz.id}.service" + controller_modules = controller_modules = { + "default": f"cryptoadvance.specter.services.{clazz.id}.controller" + } + + only_one_blueprint = len(controller_modules.items()) == 1 + + def inject_stuff(): + """Can be used in all jinja2 templates""" + return dict(specter=app.specter, service=ext) + + if "default" not in controller_modules.keys(): + raise SpecterError( + "You need at least one Blueprint, with the key 'default'. It will be used to link to your UI" + ) + + for bp_key, bp_value in controller_modules.items(): + if bp_key == "": + raise SpecterError("Empty keys are not allowed in the blueprints map") + middple_part = "" if bp_key == "default" else f"{bp_key}_" + bp_name = f"{clazz.id}_{middple_part}endpoint" + logger.debug( + f" Creating blueprint with name {bp_name} (middle_part:{middple_part}:" + ) + bp = Blueprint( + f"{clazz.id}_{middple_part}endpoint", + bp_value, + template_folder=get_template_static_folder("templates"), + static_folder=get_template_static_folder("static"), + ) + if only_one_blueprint: + setattr(clazz, "blueprint", bp) + else: + clazz.blueprints[bp_key] = bp + bp.context_processor(inject_stuff) + + # Import the controller for this service + logger.info(f" Loading Controller {bp_value}") + + try: + controller_module = import_module(bp_value) + except ModuleNotFoundError as e: + raise Exception( + f""" + There was an issue finding a controller module: + {e} + That module was specified in the Service class of service {clazz.id} + check that specification in {clazz.__module__} + """ + ) + + # finally register the blueprint + if clazz.isolated_client: + ext_prefix = app.config["ISOLATED_CLIENT_EXT_URL_PREFIX"] + else: + ext_prefix = app.config["EXT_URL_PREFIX"] + + try: + bp_postfix = "" if only_one_blueprint else f"/{bp_key}" + if ( + app.testing + and len( + [vf for vf in app.view_functions if vf.startswith(clazz.id)] + ) + <= 1 + ): # the swan-static one + # Yet again that nasty workaround which has been described in the archblog. + # The easy variant can be found in server.py + # The good news is, that we'll only do that for testing + import importlib + + logger.info("Reloading Extension controller") + importlib.reload(controller_module) + app.register_blueprint( + bp, url_prefix=f"{ext_prefix}/{clazz.id}{bp_postfix}" + ) + else: + app.register_blueprint( + bp, url_prefix=f"{ext_prefix}/{clazz.id}{bp_postfix}" + ) + logger.info(f" Mounted {bp} to {ext_prefix}/{clazz.id}{bp_postfix}") + except AssertionError as e: + if str(e).startswith("A name collision"): + raise SpecterError( + f""" + There is a name collision for the {clazz.blueprint.name}. \n + This is probably because you're running in DevelopementConfig and configured + the extension at the same time in the EXTENSION_LIST which currently loks like this: + {app.config['EXTENSION_LIST']}) + """ + ) + + @classmethod + def add_devices_for_ext(cls, clazz, ext): + if hasattr(clazz, "devices"): + devices_modules = clazz.devices + else: + return + classes = [] + for module in devices_modules: + try: + classes.extend( + get_classlist_of_type_clazz_from_modulelist(Device, [module]) + ) + except ModuleNotFoundError: + raise SpecterError( + f""" + The extension {ext.id} declared devices and a module called {module} + But that module is not existing. + """ + ) + if len(classes) == 0: + raise SpecterError( + f""" + The extension {ext.id} declared devices and a module called {module} + But that module doesn't contain any devices. + """ + ) + from cryptoadvance.specter.devices import __all__ as all_devices + + for device_class in classes: + all_devices.append(device_class) + logger.debug(f" Loaded Device {device_class}") + + @classmethod + def configure_service_for_module(cls, clazz): + """searches for ConfigClasses in the module-Directory and merges its config in the global config""" + try: + module = import_module(f"cryptoadvance.specter.services.{clazz.id}.config") + except ModuleNotFoundError: + # maybe the other style: + org = clazz.__module__.split(".")[0] + try: + module = import_module(f"{org}.specterext.{clazz.id}.config") + except ModuleNotFoundError: + logger.warning( + f" Service {clazz.id} does not have a service Configuration! Skipping!" + ) + return + main_config_clazz_name = app.config.get("SPECTER_CONFIGURATION_CLASS_FULLNAME") + main_config_clazz_slug = main_config_clazz_name.split(".")[-1] + potential_config_classes = [] + for attribute_name in dir(module): + attribute = getattr(module, attribute_name) + if isclass(attribute): + clazz = attribute + potential_config_classes.append(clazz) + if ( + clazz.__name__.split(".")[-1] == main_config_clazz_slug + ): # e.g. BaseConfig or DevelopmentConfig + cls.import_config(clazz) + return + + config_module = import_module(".".join(main_config_clazz_name.split(".")[0:-1])) + + config_clazz = getattr(config_module, main_config_clazz_slug) + config_candidate_class = config_clazz.__bases__[0] + while config_candidate_class != object: + for clazz in potential_config_classes: + if clazz.__name__.split(".")[-1] == config_candidate_class.__name__: + cls.import_config(clazz) + return + config_candidate_class = config_candidate_class.__bases__[0] + logger.warning( + f"Could not find a configuration for Service {module}. Skipping configuration." + ) + + @classmethod + def import_config(cls, clazz): + logger.info(f" Loading Service-specific configuration from {clazz}") + for key in dir(clazz): + if key.isupper(): + if app.config.get(key): + raise Exception( + f"Config {clazz} tries to override existing key {key}" + ) + app.config[key] = getattr(clazz, key) + logger.debug(f" setting {key} = {app.config[key]}") + + def execute_ext_callbacks(self, callback_id, *args, **kwargs): + """will execute the callback function for each extension which has defined that method + the callback_id needs to be passed and specify why the callback has been called. + It needs to be one of the constants defined in cryptoadvance.specter.services.callbacks + """ + if callback_id not in dir(callbacks): + raise Exception(f"Non existing callback_id: {callback_id}") + # No debug statement here possible as this is called for every request and would flood the logs + # logger.debug(f"Executing callback {callback_id}") + return_values = {} + for ext in self.services.values(): + if hasattr(ext, f"callback_{callback_id}"): + return_values[ext.id] = getattr(ext, f"callback_{callback_id}")( + *args, **kwargs + ) + elif hasattr(ext, "callback"): + return_values[ext.id] = ext.callback(callback_id, *args, **kwargs) + # Filtering out all None return values + return_values = {k: v for k, v in return_values.items() if v is not None} + # logger.debug(f"return_values for callback {callback_id} {return_values}") + return return_values + + @property + def services(self) -> Dict[str, Service]: + return self._services or {} + + @property + def services_sorted(self): + service_names = sorted( + self._services, key=lambda s: self._services[s].sort_priority + ) + return [self._services[s] for s in service_names] + + def user_has_encrypted_storage(self, user: User) -> bool: + """Looks for any data for any service in the User's ServiceEncryptedStorage. + This check works even if the user doesn't have their plaintext_user_secret + available.""" + encrypted_data = ( + app.specter.service_encrypted_storage_manager.get_raw_encrypted_data(user) + ) + print(f"encrypted_data: {encrypted_data} for {user}") + return encrypted_data != {} + + def set_active_services(self, service_names_active): + logger.debug(f"Setting these services active: {service_names_active}") + self.specter.update_services(service_names_active) + for _, ext in self.services.items(): + logger.debug( + f"Setting service '{ext.id}' active to {ext.id in service_names_active}" + ) + ext.active = ext.id in service_names_active + + def get_service(self, plugin_id: str) -> Service: + """get an extension-instance by ID. Raises an ExtensionException if it doesn't find it.""" + if plugin_id not in self._services: + raise ExtensionException(f"No such plugin: '{plugin_id}'") + return self._services[plugin_id] + + def remove_all_services_from_user(self, user: User): + """ + Clears User.services and `user_secret`; wipes the User's + ServiceEncryptedStorage. + """ + # Don't show any Services on the sidebar for the admin user + user.services.clear() + + # Reset as if we never had any encrypted storage + user.delete_user_secret(autosave=False) + user.save_info() + + if self.user_has_encrypted_storage(user=user): + # Encrypted Service data is now orphaned since there is no + # password. So wipe it from the disk. + app.specter.service_encrypted_storage_manager.delete_all_service_data(user) + + @classmethod + def get_service_x_dirs(cls, x): + """returns a list of package-directories which each represents a specific service. + This is used EXCLUSIVELY by the pyinstaller-hook packaging specter to add templates/static + When this gets called, CWD is ./pyinstaller + """ + + arr = [ + Path(Path(_get_module_from_class(clazz).__file__).parent, x) + for clazz in get_subclasses_for_clazz(Service) + ] + logger.debug(f"Initial arr:") + for element in arr: + logger.debug(element) + # /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/swan/templates + + arr = [path for path in arr if path.is_dir()] + # Those pathes are absolute. Let's make them relative: + arr = [Path(*path.parts[-6:]) for path in arr] + + virtuelenv_path = os.path.relpath(os.environ["VIRTUAL_ENV"], ".") + + if os.name == "nt": + virtualenv_search_path = Path(virtuelenv_path, "Lib") + else: + # let's calcultate so that we get something like: + # virtualenv_search_path = Path("..", ".buildenv", "lib", "python3.8") + site_package = [path for path in sys.path if "site-packages" in path][0] + site_package = Path(virtuelenv_path, *(Path(site_package).parts[-3:-1])) + virtualenv_search_path = site_package + + # ... and as the classes are in the .buildenv (see build-unix.sh) let's add .. + arr = [Path(virtualenv_search_path, path) for path in arr] + + # Non internal-repo extensions sitting in org/specterext/... need to be added, too + src_org_specterext_exts = search_dirs_in_path( + virtualenv_search_path, return_without_extid=False + ) + src_org_specterext_exts = [Path(path, x) for path in src_org_specterext_exts] + + arr.extend(src_org_specterext_exts) + + return arr + + @classmethod + def get_service_packages(cls): + """returns a list of strings containing the service-classes (+ controller/config-classes) + This is used for hiddenimports in pyinstaller + """ + arr = get_subclasses_for_clazz(Service) + arr.extend( + get_classlist_of_type_clazz_from_modulelist( + Service, ProductionConfig.EXTENSION_LIST + ) + ) + arr = [clazz.__module__ for clazz in arr] + # Controller-Packagages from the services are not imported via the service but via the baseclass + # Therefore hiddenimport don't find them. We have to do it here. + cont_arr = [ + ".".join(package.split(".")[:-1]) + ".controller" for package in arr + ] + for controller_package in cont_arr: + try: + import_module(controller_package) + arr.append(controller_package) + except ImportError: + pass + except AttributeError: + # something like: + # AttributeError: type object 'BitcoinReserveService' has no attribute 'blueprint' + # shows that the package is existing + arr.append(controller_package) + except RuntimeError: + # something like + # RuntimeError: Working outside of application context. + # shows that the package is existing + arr.append(controller_package) + config_arr = [".".join(package.split(".")[:-1]) + ".config" for package in arr] + for config_package in config_arr: + try: + import_module(config_package) + arr.append(config_package) + except ModuleNotFoundError as e: + pass + return arr diff --git a/src/cryptoadvance/specter/managers/service_manager/__init__.py b/src/cryptoadvance/specter/managers/service_manager/__init__.py new file mode 100644 index 0000000000..75695a5caf --- /dev/null +++ b/src/cryptoadvance/specter/managers/service_manager/__init__.py @@ -0,0 +1 @@ +from .service_manager import ExtensionManager diff --git a/src/cryptoadvance/specter/managers/service_manager/callback_executor.py b/src/cryptoadvance/specter/managers/service_manager/callback_executor.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/cryptoadvance/specter/managers/service_manager/service_manager.py b/src/cryptoadvance/specter/managers/service_manager/service_manager.py index 630dfe1eaa..5fbca682e1 100644 --- a/src/cryptoadvance/specter/managers/service_manager/service_manager.py +++ b/src/cryptoadvance/specter/managers/service_manager/service_manager.py @@ -17,21 +17,21 @@ from flask import url_for from flask.blueprints import Blueprint -from ..services.service import Service -from ..services import callbacks, ExtensionException -from ..util.reflection import ( +from ...services.service import Service +from ...services import callbacks, ExtensionException +from ...util.reflection import ( _get_module_from_class, get_classlist_of_type_clazz_from_modulelist, get_package_dir_for_subclasses_of, get_subclasses_for_clazz, get_subclasses_for_clazz_in_cwd, ) -from ..util.reflection_fs import search_dirs_in_path +from ...util.reflection_fs import search_dirs_in_path logger = logging.getLogger(__name__) -class ServiceManager: +class ExtensionManager: """Loads support for all Services it auto-discovers.""" def __init__(self, specter, devstatus_threshold): @@ -44,7 +44,7 @@ def __init__(self, specter, devstatus_threshold): logger.info("----> starting service discovery Static") # How do we discover services? Two configs are relevant: # * SERVICES_LOAD_FROM_CWD (boolean, CWD is current working directory) - # * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specter.services.swan.service"]) + # * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specterext.swan.service"]) # Ensuring security (especially for the CWD) is NOT done here but # in the corresponding (Production)Config logger.debug(f"EXTENSION_LIST = {app.config.get('EXTENSION_LIST')}") @@ -80,7 +80,7 @@ def __init__(self, specter, devstatus_threshold): f"Service {clazz.__name__} not activated due to devstatus ( {self.devstatus_threshold} > {clazz.devstatus} )" ) logger.info("----> finished service processing") - self.execute_ext_callbacks("afterServiceManagerInit") + self.execute_ext_callbacks("afterExtensionManagerInit") @classmethod def register_blueprint_for_ext(cls, clazz, ext): @@ -359,7 +359,6 @@ def get_service_x_dirs(cls, x): logger.debug(f"Initial arr:") for element in arr: logger.debug(element) - # /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/bitcoinreserve/templates # /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/swan/templates arr = [path for path in arr if path.is_dir()] diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index ad33547215..80c63aa189 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -5,7 +5,7 @@ from cryptoadvance.specter.hwi_rpc import HWIBridge from cryptoadvance.specter.liquid.rpc import LiquidRPC -from cryptoadvance.specter.managers.service_manager import ServiceManager +from cryptoadvance.specter.managers.service_manager import ExtensionManager from cryptoadvance.specter.rpc import BitcoinRPC from cryptoadvance.specter.services import callbacks from cryptoadvance.specter.util.reflection import get_template_static_folder @@ -147,8 +147,8 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None): # HWI specter.hwi = HWIBridge() - # ServiceManager will instantiate and register blueprints for extensions - specter.service_manager = ServiceManager( + # ExtensionManager will instantiate and register blueprints for extensions + specter.service_manager = ExtensionManager( specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"] ) diff --git a/src/cryptoadvance/specter/services/callbacks.py b/src/cryptoadvance/specter/services/callbacks.py index d2266510b4..21a3e1160d 100644 --- a/src/cryptoadvance/specter/services/callbacks.py +++ b/src/cryptoadvance/specter/services/callbacks.py @@ -1,7 +1,7 @@ """ Here we have some constants getting an id for extension-points/callbacks. As camelcase is used, we don't use CAPITAL letters to not loose the meaning of the camelcase. - These constants are expected as parameter to the ServiceManager.callback function + These constants are expected as parameter to the ExtensionManager.callback function and it'll throw an exception if the constant does not exist. There are some weak naming conventions: @@ -14,7 +14,7 @@ """ I don't know why we have this one. Doesn't seem to be used anywhere. """ -afterServiceManagerInit = "afterServiceManagerInit" +afterExtensionManagerInit = "afterExtensionManagerInit" """ This one is called, after the init_app method has finished. The "run" method has not @@ -35,6 +35,12 @@ """ add_wallettabs = "add_wallettabs" +""" Endpoints might define their behaviour via a ViewModel. Those Models are passed here and + extensions can modify that behaviour via Modifying that model. Currently there is only: + cryptoadvance.specter.server_enpoints.welcome.welcome_vm.WelcomeVm +""" +adjust_view_model = "adjust_view_model" + """ This one is called, whenever a file is persisted. To call external scripts in another process, you better use the SPECTER_PERSISTENCE_CALLBACK Env Var or it's asynchronous cousin diff --git a/src/cryptoadvance/specter/services/controller.py b/src/cryptoadvance/specter/services/controller.py index ebf5ca4996..eeefa889e0 100644 --- a/src/cryptoadvance/specter/services/controller.py +++ b/src/cryptoadvance/specter/services/controller.py @@ -21,7 +21,7 @@ # All blueprint from Services are no longer loaded statically but dynamically when the service-class in initialized # check cryptoadvance.specter.services.service_manager.Service for doing that and # check cryptoadvance.specter.services/**/manifest for instances of Service-classes and -# check cryptoadvance.specter.services.service_manager.ServiceManager.services for initialisation of ServiceClasses +# check cryptoadvance.specter.services.service_manager.ExtensionManager.services for initialisation of ServiceClasses def user_secret_decrypted_required(func): diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index afeae31c02..143a182795 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -41,7 +41,7 @@ RpcError, get_default_datadir, ) -from .managers.service_manager import ServiceManager +from .managers.service_manager import ExtensionManager from .services.service import devstatus_alpha, devstatus_beta, devstatus_prod from .specter_error import ExtProcTimeoutException, SpecterError from .tor_daemon import TorDaemonController @@ -68,7 +68,9 @@ def __init__( config={}, internal_bitcoind_version="", checker_threads=True, + initialize=True, ): + """Very basic Initialisation of the Specter Object. Make sure to call specter.initialize() shortly after""" if data_folder.startswith("~"): data_folder = os.path.expanduser(data_folder) data_folder = os.path.abspath(data_folder) @@ -78,30 +80,40 @@ def __init__( os.makedirs(data_folder) self.data_folder = data_folder - + self._config = config + self._internal_bitcoind_version = internal_bitcoind_version + self._checker_threads = checker_threads + if initialize: + self.initialize() + + def initialize(self): + """Runs the checker_treads, instantiates all the managers and attach them to its attributes""" self.user_manager = UserManager( self ) # has to come before calling VersionChecker() # version checker # checks for new versions once per hour + logger.info("Instantiate VersionChecker") self.version = VersionChecker(specter=self) - if checker_threads: + if self._checker_threads: self.version.start() - self._config_manager = ConfigManager(self.data_folder, config) + logger.info("Instantiate ConfigManager") + self._config_manager = ConfigManager(self.data_folder, self._config) - self.internal_bitcoind_version = internal_bitcoind_version + self.internal_bitcoind_version = self._internal_bitcoind_version # Migrating from Specter 1.3.1 and lower (prior to the node manager) self.migrate_old_node_format() + logger.info("Instantiate NodeManager") self.node_manager = NodeManager( proxy_url=self.proxy_url, only_tor=self.only_tor, active_node=self.active_node_alias, bitcoind_path=self.bitcoind_path, - internal_bitcoind_version=internal_bitcoind_version, + internal_bitcoind_version=self._internal_bitcoind_version, data_folder=os.path.join(self.data_folder, "nodes"), ) @@ -136,12 +148,12 @@ def __init__( self.update_tor_controller() self.checker = Checker(lambda: self.check(check_all=True), desc="health") - if checker_threads: + if self._checker_threads: self.checker.start() self.price_checker = Checker( lambda: update_price(self, self.user), desc="price" ) - if self.price_check and self.price_provider and checker_threads: + if self.price_check and self.price_provider and self._checker_threads: self.price_checker.start() # Configuring the two different storages (Universal json-files) diff --git a/src/cryptoadvance/specterext/swan/controller.py b/src/cryptoadvance/specterext/swan/controller.py index 4c21e51051..a772147bfd 100644 --- a/src/cryptoadvance/specterext/swan/controller.py +++ b/src/cryptoadvance/specterext/swan/controller.py @@ -15,8 +15,8 @@ from flask_babel import lazy_gettext as _ from flask_login import current_user, login_required -from ...server_endpoints import flash -from ..controller import user_secret_decrypted_required +from cryptoadvance.specter.server_endpoints import flash +from cryptoadvance.specter.services.controller import user_secret_decrypted_required from . import client as swan_client from .client import SwanApiException from .service import SwanService diff --git a/tests/test_managers_service.py b/tests/test_managers_service.py index f439debbc8..7505cefcd0 100644 --- a/tests/test_managers_service.py +++ b/tests/test_managers_service.py @@ -4,31 +4,30 @@ import os from unittest.mock import MagicMock from flask import Flask -from cryptoadvance.specter.managers.service_manager import ServiceManager +from cryptoadvance.specter.managers.service_manager import ExtensionManager from cryptoadvance.specter.services.callbacks import after_serverpy_init_app -def test_ServiceManager2(mock_specter, mock_flaskapp, caplog): +def test_ExtensionManager2(mock_specter, mock_flaskapp, caplog): ctx = mock_flaskapp.app_context() ctx.push() - sm = ServiceManager(mock_specter, "alpha") + sm = ExtensionManager(mock_specter, "alpha") # We have passed the TestConfig which is (hopefully) not existing in the Swan Extension - # So the ServiceManager will move up the dependency tree of TestConfig until it finds + # So the ExtensionManager will move up the dependency tree of TestConfig until it finds # a Config and will copy the keys into the flask-config assert mock_flaskapp.config["SWAN_API_URL"] == "https://api.dev.swanbitcoin.com" - assert sm.services["bitcoinreserve"] != None assert sm.services["swan"] != None sm.execute_ext_callbacks(after_serverpy_init_app, scheduler=None) @pytest.mark.skip(reason="The .buildenv directoy does not exist on the CI-Infra") -def test_ServiceManager_get_service_x_dirs(caplog): +def test_ExtensionManager_get_service_x_dirs(caplog): caplog.set_level(logging.DEBUG) try: os.chdir("./pyinstaller") # THis is usefull in the pytinstaller/specterd.spec - dirs = ServiceManager.get_service_x_dirs("templates") + dirs = ExtensionManager.get_service_x_dirs("templates") # As the tests are executed in a development-environment (pip3 install -e .), the results we get back here # are not the same than the one we would get back when really are building. Because in that case, # the .buildenv would the environment and no symlinks would link to src/cryptoadavance... @@ -43,24 +42,19 @@ def test_ServiceManager_get_service_x_dirs(caplog): for path in dirs: assert str(path).endswith("templates") - dirs = ServiceManager.get_service_x_dirs("static") + dirs = ExtensionManager.get_service_x_dirs("static") assert f"{expected_folder}/cryptoadvance/specter/services/swan/static" in [ str(dir) for dir in dirs ] - assert ( - f"{expected_folder}/cryptoadvance/specter/services/bitcoinreserve/static" - in [str(dir) for dir in dirs] - ) print(dirs) assert len(dirs) >= 2 finally: os.chdir("../") -def test_ServiceManager_get_service_packages(): - packages = ServiceManager.get_service_packages() - assert "cryptoadvance.specter.services.swan.service" in packages - assert "cryptoadvance.specter.services.bitcoinreserve.service" in packages +def test_ExtensionManager_get_service_packages(): + packages = ExtensionManager.get_service_packages() + assert "cryptoadvance.specterext.swan.service" in packages @pytest.fixture @@ -75,12 +69,11 @@ def mock_flaskapp(mock_specter): flaskapp_mock = Flask(__name__) flaskapp_mock.config["EXTENSION_LIST"] = [ - "cryptoadvance.specter.services.swan.service", - "cryptoadvance.specter.services.bitcoinreserve.service", + "cryptoadvance.specterext.swan.service", ] flaskapp_mock.config["ISOLATED_CLIENT_EXT_URL_PREFIX"] = "/spc/ext" flaskapp_mock.config["EXT_URL_PREFIX"] = "/ext" - # The ServiceManager is a flask-aware component. It will load all the services + # The ExtensionManager is a flask-aware component. It will load all the services # however, in order to configure them, he needs to know about the configuration # of the specterApp. flaskapp_mock.config[ diff --git a/tests/test_services.py b/tests/test_services.py index 65bcaf7506..db92a3bae5 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -19,7 +19,7 @@ ServiceEncryptedStorageError, ServiceEncryptedStorageManager, ) -from cryptoadvance.specter.managers.service_manager import ServiceManager +from cryptoadvance.specter.managers.service_manager import ExtensionManager from cryptoadvance.specter.user import User, hash_password @@ -31,13 +31,13 @@ class FakeService(Service): # @patch("cryptoadvance.specter.services.service_manager.app") -# def test_ServiceManager_loads_services(empty_data_folder, app): +# def test_ExtensionManager_loads_services(empty_data_folder, app): # # app.config = MagicMock() # # app.config.get.return_value = "prod" # specter_mock = MagicMock() # specter_mock.data_folder.return_value = empty_data_folder -# service_manager = ServiceManager(specter=specter_mock, devstatus_threshold="alpha") +# service_manager = ExtensionManager(specter=specter_mock, devstatus_threshold="alpha") # services = service_manager.services # assert "swan" in services