from typing import TYPE_CHECKING if TYPE_CHECKING: from .noSysCore import NoSysCore import sys import importlib import logging from typing import Dict, Tuple, Any from .events import Events from .noSysModule import NoSysModule from libs.app.common.logging import get_logger from libs.fspn.protocol.security import Pmc logger = get_logger("moduleManager", buffer=True) class ModuleManager: """Loads, unloads, and manages modules dynamically.""" def __init__(self, nosys_core: "NoSysCore"): self.nosys_core = nosys_core self.modules: Dict[str, NoSysModule] = {} from libs.api.api import Api self.api:Api = None self.pmc:Pmc = None self.load_from_config() def get(self, package_id, module_id) -> NoSysModule: return self.modules.get((package_id, module_id)) def load_from_config(self): libs = self.nosys_core.config.get("app", "libs") for lib in libs: if lib.get("enabled", True): for module in self.nosys_core.config.get(lib.get("id"), "info", "modules"): self.load(lib.get("id"), module.get("id")) def load(self, package_id: str, module_id: str): """Dynamically load a module and initialize it.""" logger.debug(f"Importing module: {package_id}.{module_id}") key = (package_id, module_id) if key in self.modules: return self.modules[key] path = f"libs.{package_id}.{module_id}" try: if path in sys.modules: module = importlib.reload(sys.modules[path]) else: module = importlib.import_module(path) # Prefer explicit "get_module_class" function if provided if hasattr(module, "get_module_class"): clss = module.get_module_class() else: # Fallback to naming convention: Uppercase the first letter of the module class_name = module_id[:1].upper()+module_id[1:] clss = getattr(module, class_name) if not issubclass(clss, NoSysModule): raise TypeError(f"{clss} must inherit from NoSysModule") instance = clss(self.nosys_core) self.modules[key] = instance logger.debug(f"Module loaded: {path} ({clss.__name__})") return instance except Exception as e: logger.exception(f"Failed to load module {path}: {e}") return None def unload(self, package_id: str, module_id: str): """Unload a module and call its teardown hook if available.""" key = (package_id, module_id) if key not in self.modules: return module:NoSysModule = self.modules.pop(key) try: module.teardown_module() except Exception as e: logger.warning(f"Error during teardown of {key}: {e}") logger.debug(f"Module unloaded: {key}") def reload(self, package_id: str, module_id: str): """Reload a module (unload + load).""" self.unload(package_id, module_id) return self.load(package_id, module_id) def setup(self): for module in self.modules.values(): module.setup()