135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
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) |