import sys import subprocess import urllib.request import os import json import types import logging import time import zipfile from pathlib import Path from threading import Thread from libs.app.common.config import Config from libs.app.common.logging import get_logger from libs.app.common.paths import LIBS_DIR logger = get_logger("updater", buffer=True) class Updater: """ Updater class responsible for managing library updates. """ def __init__(self, config:Config): self.config = config self.repositories = self.config.get("app", "repositories", default=[]) self.updated_libs: dict[str, any] = {} self._event_listeners: list[callable] = [] def update_libs(self): """Check and update all configured libraries in parallel.""" logger.debug("Checking libraries updates...") threads: list[Thread] = [] for lib in self.config.get("app", "libs", default=[]): lib_id = lib.get("id") self._update_lib_status(lib_id, "Reading configuration") if lib.get("update", {"checkUpdates":True}).get("checkUpdates", True): self._update_lib_status(lib_id, "Checking updates") thread = Thread(target=self.check_lib_update, args=(lib,)) thread.start() threads.append(thread) else: self._update_lib_status(lib_id, "Checking updates disabled") for thread in threads: thread.join() return self.updated_libs def check_lib_update(self, lib:dict): """ Check for a library update in repositories and install if newer version is available. """ lib_id = lib.get("id") lib_config = self.config.get(lib_id) lib_repositories = lib.get("update", {"repositories":[]}).get("repositories", []) lib_path = LIBS_DIR / lib_id if not lib_config: Path(lib_path).mkdir(parents=True, exist_ok=True) current_version = 0 else: current_version = lib_config.get("info").get("version") logger.debug(f"Checking updates of lib {lib_id} with current version {current_version}") repositories = list(self.repositories) repositories.extend(lib_repositories) latest_version = (current_version, None) for repo in repositories: try: info_url = f"{repo}/{lib_id}/info.json" logger.debug(f"Reading remote info of {lib_id} from repository {repo}") with urllib.request.urlopen(info_url, timeout=2) as response: remote_info = json.load(response) logger.debug(f"{lib_id} remote info: {remote_info}") remote_version = remote_info.get("version") if remote_version > latest_version[0]: latest_version = (remote_version, f"{repo}/{lib_id}") except Exception as e: logger.error(f"Error reading remote info of {lib_id} from {repo}: {e}") if latest_version[1]: lib_url = f"{latest_version[1]}/{lib_id}.zip" lib_file = LIBS_DIR / f"{lib_id}.zip" logger.debug(f"Downloading lib from {lib_url} to {lib_file}") self._update_lib_status(lib_id, f"Downloading lastest lib version {latest_version[0]}") self.download_lib(lib_url, lib_file) self._update_lib_status(lib_id, f"Installing lib requirements") self.pip_install_requirements(lib_path) self._update_lib_status(lib_id, f"Success updated") else: self._update_lib_status(lib_id, f"Current version is the latest") def download_lib(self, url: str, path: str) -> None: """Download and extract a lib from repository.""" urllib.request.urlretrieve(url, path) logger.info(f"Extracting all from {path}") with zipfile.ZipFile(path, "r") as zip_ref: zip_ref.extractall(Path(path).parent.absolute()) def pip_install_requirements(self, lib_path: str) -> None: """Run pip install on requirements.txt if present.""" requirements_path = os.path.join(lib_path, "requirements.txt") if os.path.exists(requirements_path): logger.info(f"Installing requirements from {requirements_path}") subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_path]) else: logger.info(f"No requirements.txt found for {lib_path}") def register_listener(self, callback: callable): """ Register a listener callback that will be called with each new event. """ if callback not in self._event_listeners: self._event_listeners.append(callback) def emit_event(self, **kwargs): event = types.SimpleNamespace() for k, v in kwargs.items(): setattr(event, k, v) for listener in self._event_listeners: listener(event) def _update_lib_status(self, lib, status): self.updated_libs[lib] = status self.emit_event(name="status_lib", lib=lib, status=status)