diff --git a/full_updater/app.py b/full_updater/app.py index 9797244..9a477d4 100644 --- a/full_updater/app.py +++ b/full_updater/app.py @@ -10,7 +10,8 @@ from textual import work from full_updater.backend.cache import ensure_cache_dir, read_cache, get_cache_timestamp from full_updater.backend.scanner import ( Target, ScanResult, get_lxc_list, lxc_is_running, - ensure_debsecan_installed, scan_apt, scan_cve, write_cache + ensure_debsecan_installed, scan_apt, scan_cve, write_cache, + run_full_scan ) from full_updater.backend.executor import UpgradeExecutor from full_updater.ui.loader import LoaderScreen @@ -67,61 +68,19 @@ class FullUpdaterApp(App): pct = min(100.0, (completed / (total * 4)) * 100) self.call_from_thread(self._update_loader, pct) - for idx, target in enumerate(self.targets): - self.call_from_thread(self._update_loader_status, idx, "running") - - if not target.is_host and not lxc_is_running(target.target_id): - self.call_from_thread(self._update_loader_status, idx, "skipped") - result = ScanResult(target=target) - result.status = "skipped" - self.results[target.target_id] = result - for _ in range(4): - progress_cb() - continue - - debsecan_ok, debsecan_err = ensure_debsecan_installed(target.is_host, target.target_id) - progress_cb() - - apt_ok, apt_packages, apt_err = scan_apt(target) - progress_cb() - - cve_ok = False - cve_list = [] - cve_err = "" - if debsecan_ok: - cve_ok, cve_list, cve_err = scan_cve(target) - else: - cve_err = f"debsecan: {debsecan_err}" - progress_cb() - - result = ScanResult( - target=target, - apt_ok=apt_ok, - cve_ok=cve_ok, - apt_count=len(apt_packages), - cve_count=len(cve_list), - apt_packages=apt_packages, - cve_list=cve_list, - error=apt_err or cve_err, - status="done" if (apt_ok and (cve_ok or not debsecan_ok)) else "error" - ) - if result.error: - result.status = "error" - self.results[target.target_id] = result - - cache_id = "host" if target.is_host else target.target_id - write_cache(cache_id, { - "timestamp": datetime.now().isoformat(), - "apt_count": result.apt_count, - "apt_packages": result.apt_packages, - "cve_count": result.cve_count, - "cve_list": result.cve_list, - "error": result.error - }) - + def on_result(result: ScanResult): + self.results[result.target.target_id] = result + idx = 0 + for i, t in enumerate(self.targets): + if t.target_id == result.target.target_id: + idx = i + break self.call_from_thread(self._update_loader_status, idx, result.status) - self.call_from_thread(self._update_sidebar, target.target_id, result) + self.call_from_thread(self._update_sidebar, result.target.target_id, result) + results = run_full_scan(progress_cb) + for r in results: + on_result(r) self.call_from_thread(self._finish_scan) def _update_loader(self, pct: float): @@ -157,8 +116,12 @@ class FullUpdaterApp(App): res.error, res.status == "skipped" ) + # Forcer le rafraichissement de l'interface + sidebar.refresh() if self.targets: self._select_target(self.targets[0].target_id) + summary = self.query_one(SummaryPanel) + summary.refresh() def _select_target(self, target_id: str): self.selected_target = target_id @@ -179,6 +142,7 @@ class FullUpdaterApp(App): skipped=not data and any(t.target_id == target_id and not t.is_host and not lxc_is_running(t.target_id) for t in self.targets), cache_time=get_cache_timestamp(cache_id) ) + summary.refresh() def on_sidebar_target_selected(self, event: Sidebar.TargetSelected): self._select_target(event.target_id) diff --git a/full_updater/backend/scanner.py b/full_updater/backend/scanner.py index 2f738fe..9462621 100644 --- a/full_updater/backend/scanner.py +++ b/full_updater/backend/scanner.py @@ -1,7 +1,5 @@ -import asyncio -import json +import concurrent.futures import re -import shutil import subprocess from dataclasses import dataclass, field from typing import Any @@ -65,7 +63,6 @@ def lxc_is_running(vmid: str) -> bool: def ensure_debsecan_installed(is_host: bool, vmid: str = "") -> tuple[bool, str]: """Retourne (ok, error_msg)""" if is_host: - # Vérifier si debsecan est installé sur l'hôte ok, _, _ = run_cmd(["which", "debsecan"]) if ok: return True, "" @@ -77,7 +74,6 @@ def ensure_debsecan_installed(is_host: bool, vmid: str = "") -> tuple[bool, str] return False, f"apt-get install debsecan failed: {err}" return True, "" else: - # Vérifier dans le LXC ok, _, _ = run_cmd(["pct", "exec", vmid, "--", "which", "debsecan"]) if ok: return True, "" @@ -108,12 +104,10 @@ def scan_apt(target: Target) -> tuple[bool, list[dict[str, str]], str]: if len(parts) < 2: continue name = parts[0].strip() - rest = parts[1] - # Format: apt list --upgradable -> name/arch version suite [upgradable from: oldver] + # Essayer d'extraire la nouvelle version et l'ancienne m = re.search(r"\[upgradable from:\s+([^\]]+)\]", line) if m: old_ver = m.group(1).strip() - # Essayer d'extraire la nouvelle version (avant le [upgradable from:]) prefix = line.split("[upgradable from:")[0].strip() ver_m = re.search(r"/\S+\s+(\S+)\s+\S+", prefix) new_ver = ver_m.group(1) if ver_m else "?" @@ -124,7 +118,6 @@ def scan_apt(target: Target) -> tuple[bool, list[dict[str, str]], str]: "size": "-" }) else: - # fallback simple packages.append({ "name": name, "current": "?", @@ -146,7 +139,6 @@ def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: cves = [] for line in stdout.splitlines(): - # Format debsecan: CVE-XXXX-XXXX package [remote] [low] - description m = re.match(r"(CVE-\d{4}-\d+)\s+(\S+)", line) if m: cve_id = m.group(1) @@ -159,38 +151,25 @@ def scan_cve(target: Target) -> tuple[bool, list[dict[str, str]], str]: return True, cves, "" -async def scan_target(target: Target, result: ScanResult, progress_cb: Any) -> None: - """Scanne une cible (APT puis CVE) et écrit le cache.""" +def scan_single_target(target: Target, progress_cb: Any) -> ScanResult: + """Scanne une cible (APT + CVE) et retourne le résultat.""" + result = ScanResult(target=target) result.status = "running" - await progress_cb() + progress_cb() - # Vérifier si LXC est allumé if not target.is_host and not lxc_is_running(target.target_id): result.status = "skipped" - result.apt_ok = False - result.cve_ok = False - write_cache(target.target_id, { - "timestamp": "", - "apt_count": 0, - "apt_packages": [], - "cve_count": 0, - "cve_list": [], - "error": "LXC éteint" - }) - await progress_cb() - return + return result - # Auto-install debsecan debsecan_ok, debsecan_err = ensure_debsecan_installed(target.is_host, target.target_id) + progress_cb() - # Scan APT apt_ok, apt_packages, apt_err = scan_apt(target) result.apt_ok = apt_ok result.apt_count = len(apt_packages) result.apt_packages = apt_packages - await progress_cb() + progress_cb() - # Scan CVE if debsecan_ok: cve_ok, cve_list, cve_err = scan_cve(target) result.cve_ok = cve_ok @@ -200,29 +179,48 @@ async def scan_target(target: Target, result: ScanResult, progress_cb: Any) -> N result.error = cve_err else: result.cve_ok = False - result.cve_count = 0 - result.cve_list = [] result.error = f"debsecan: {debsecan_err}" - await progress_cb() + progress_cb() result.status = "done" if (result.apt_ok and result.cve_ok) else "error" if result.error: result.status = "error" - write_cache(target.target_id if not target.is_host else "host", { - "timestamp": "", # sera rempli par l'appelant + cache_id = "host" if target.is_host else target.target_id + from datetime import datetime + write_cache(cache_id, { + "timestamp": datetime.now().isoformat(), "apt_count": result.apt_count, "apt_packages": result.apt_packages, "cve_count": result.cve_count, "cve_list": result.cve_list, "error": result.error }) - await progress_cb() + progress_cb() + return result -async def run_full_scan(progress_cb: Any) -> list[ScanResult]: +def run_full_scan(progress_cb: Any) -> list[ScanResult]: targets = [Target(target_id="host", name="hote", is_host=True)] + get_lxc_list() - results = [ScanResult(target=t) for t in targets] - tasks = [scan_target(r.target, r, progress_cb) for r in results] - await asyncio.gather(*tasks) + results = [] + completed = 0 + total = len(targets) + + def _progress(): + nonlocal completed + completed += 1 + pct = min(100.0, (completed / (total * 4)) * 100) + progress_cb(pct) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + futures = {executor.submit(scan_single_target, t, _progress): t for t in targets} + for future in concurrent.futures.as_completed(futures): + try: + result = future.result() + results.append(result) + except Exception as e: + target = futures[future] + result = ScanResult(target=target, status="error", error=str(e)) + results.append(result) + return results diff --git a/full_updater/ui/cve_table.py b/full_updater/ui/cve_table.py index ae4a144..c12d1d4 100644 --- a/full_updater/ui/cve_table.py +++ b/full_updater/ui/cve_table.py @@ -28,18 +28,15 @@ class CVETable(Vertical): def __init__(self): super().__init__() - self.table = None + self.table = DataTable(id="cve-table") self.urls = {} def compose(self): with Horizontal(id="cve-toolbar"): yield Button("⬅ Retour", id="cve-back") - self.table = DataTable(id="cve-table") - yield self.table - - def on_mount(self): self.table.add_columns("CVE-ID", "Paquet", "Lien") self.table.cursor_type = "row" + yield self.table def load_data(self, cves: list[dict]): self.table.clear() diff --git a/full_updater/ui/package_table.py b/full_updater/ui/package_table.py index 25f4fcd..274f94c 100644 --- a/full_updater/ui/package_table.py +++ b/full_updater/ui/package_table.py @@ -22,16 +22,13 @@ class PackageTable(Vertical): def __init__(self): super().__init__() - self.table = None + self.table = DataTable(id="pkg-table") def compose(self): with Horizontal(id="pkg-toolbar"): yield Button("⬅ Retour", id="pkg-back") - self.table = DataTable(id="pkg-table") - yield self.table - - def on_mount(self): self.table.add_columns("Nom", "Version actuelle", "Nouvelle version", "Taille") + yield self.table def load_data(self, packages: list[dict]): self.table.clear()