From 084d1f5b422355a8e8a2771f8d5a9790bc470fc0 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 27 May 2026 07:41:44 +0200 Subject: [PATCH] fix: fix Aur detection packages --- lib/aur_manager.py | 110 +++++++++++++++++++++++++++++++++++++++++++++ lib/daemon.py | 59 ++++++++++++++++++++++++ lib/state.py | 1 + lib/ui.py | 66 ++++++++++++++++++++++++++- 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 lib/aur_manager.py diff --git a/lib/aur_manager.py b/lib/aur_manager.py new file mode 100644 index 0000000..cdd7cce --- /dev/null +++ b/lib/aur_manager.py @@ -0,0 +1,110 @@ +"""Gestion des mises à jour AUR. + +Les paquets AUR sont toujours installés immédiatement (jamais reportés à +l'extinction) car le build requiert un utilisateur actif et un accès réseau. +""" + +import logging +import os +import pwd +import shutil +import subprocess +from pathlib import Path + +from package_manager import Package, InstallResult + +logger = logging.getLogger(__name__) + +_AUR_HELPERS = ["paru", "yay"] + + +def detect_aur_helper() -> str | None: + for helper in _AUR_HELPERS: + if shutil.which(helper): + return helper + return None + + +def detect_active_user() -> str | None: + """Retourne le nom de l'utilisateur avec une session graphique active.""" + try: + result = subprocess.run( + ["loginctl", "list-sessions", "--no-legend"], + capture_output=True, text=True, timeout=5, + ) + for line in result.stdout.strip().splitlines(): + parts = line.split() + if len(parts) < 3: + continue + session_id, _, user = parts[0], parts[1], parts[2] + if user == "root": + continue + session_type = subprocess.run( + ["loginctl", "show-session", session_id, "-p", "Type", "--value"], + capture_output=True, text=True, timeout=3, + ).stdout.strip() + if session_type in ("x11", "wayland", "mir"): + return user + except Exception as e: + logger.debug(f"Impossible de détecter l'utilisateur actif : {e}") + return None + + +def _user_env(user: str) -> dict: + pw = pwd.getpwnam(user) + return { + "HOME": pw.pw_dir, + "USER": user, + "LOGNAME": user, + "XDG_RUNTIME_DIR": f"/run/user/{pw.pw_uid}", + "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + } + + +def check_aur_updates(user: str, helper: str) -> list[Package]: + """Liste les mises à jour AUR disponibles sans rien installer.""" + try: + result = subprocess.run( + ["sudo", "-u", user, "-H", helper, "-Qua", "--color", "never"], + capture_output=True, text=True, timeout=120, + env=_user_env(user), + ) + packages = [] + for line in result.stdout.strip().splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[2] == "->": + packages.append(Package( + name=parts[0], + old_version=parts[1], + new_version=parts[3], + )) + return packages + except Exception as e: + logger.error(f"Échec de la vérification AUR : {e}") + return [] + + +def install_aur_packages(user: str, helper: str, names: list[str]) -> InstallResult: + """Installe des paquets AUR spécifiques en tant qu'utilisateur actif.""" + if not names: + return InstallResult(success=True) + try: + result = subprocess.run( + [ + "sudo", "-u", user, "-H", + helper, "-S", "--noconfirm", "--noprogressbar", + "--color", "never", + ] + names, + capture_output=True, text=True, timeout=900, + env=_user_env(user), + ) + if result.returncode == 0: + return InstallResult(success=True, installed=names) + return InstallResult( + success=False, + failed=names, + error=result.stderr[-800:], + ) + except Exception as e: + logger.exception(f"Erreur lors de l'installation AUR : {e}") + return InstallResult(success=False, failed=names, error=str(e)) diff --git a/lib/daemon.py b/lib/daemon.py index 434bc24..de08f78 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -5,6 +5,10 @@ import os from datetime import datetime import state as state_mod +from aur_manager import ( + check_aur_updates, detect_active_user, detect_aur_helper, + install_aur_packages, +) from classifier import classify from config import FIRST_RUN_DELAY_SECONDS, SOCKET_PATH, UPDATE_INTERVAL_SECONDS from package_manager import ( @@ -37,6 +41,8 @@ class Daemon: logger.info("pacman est verrouillé (autre processus actif), cycle ignoré") return + asyncio.create_task(self.aur_cycle()) + try: self._set_status("checking") logger.info("Vérification des mises à jour disponibles…") @@ -125,6 +131,56 @@ class Daemon: self._add_error(str(e)) self._set_status("error") + async def aur_cycle(self): + helper = await asyncio.to_thread(detect_aur_helper) + if not helper: + logger.debug("Aucun helper AUR trouvé (paru/yay), AUR ignoré") + return + + user = await asyncio.to_thread(detect_active_user) + if not user: + logger.info("Aucune session graphique active, AUR ignoré ce cycle") + return + + logger.info(f"Vérification des mises à jour AUR avec {helper} (user={user})…") + packages = await asyncio.to_thread(check_aur_updates, user, helper) + + # Fusionner avec les paquets AUR en attente du cycle précédent + existing = {p["name"] for p in self._state.get("pending_aur", [])} + for pkg in packages: + if pkg.name not in existing: + self._state.setdefault("pending_aur", []).append({ + "name": pkg.name, + "old_version": pkg.old_version, + "new_version": pkg.new_version, + "queued_at": datetime.now().isoformat(), + }) + existing.add(pkg.name) + self._save() + + all_aur = self._state.get("pending_aur", []) + if not all_aur: + logger.info("Aucune mise à jour AUR disponible") + return + + names = [p["name"] for p in all_aur] + logger.info(f"{len(names)} mise(s) à jour AUR : {', '.join(names)}") + + result = await asyncio.to_thread(install_aur_packages, user, helper, names) + + if result.success: + logger.info("Mises à jour AUR installées avec succès") + now = datetime.now().isoformat() + for pkg in all_aur: + self._state["installed_history"].append({**pkg, "installed_at": now}) + self._state["installed_history"] = self._state["installed_history"][-200:] + self._state["pending_aur"] = [] + else: + logger.error(f"Échec installation AUR : {result.error}") + self._add_error(f"Échec AUR : {result.error}") + + self._save() + async def install_pending_cycle(self): if self._state["status"] not in ("idle", "error"): logger.info("Une opération est déjà en cours, installation ignorée") @@ -190,6 +246,9 @@ class Daemon: elif action == "install_pending": asyncio.create_task(self.install_pending_cycle()) response = {"status": "ok", "message": "Installation lancée"} + elif action == "install_aur": + asyncio.create_task(self.aur_cycle()) + response = {"status": "ok", "message": "Installation AUR lancée"} elif action == "get_state": response = {"status": "ok", "state": self._state} else: diff --git a/lib/state.py b/lib/state.py index cfd636e..cae3744 100644 --- a/lib/state.py +++ b/lib/state.py @@ -12,6 +12,7 @@ _DEFAULTS = { "last_check": None, "available_updates": [], "pending_restart": [], + "pending_aur": [], "installed_history": [], "errors": [], } diff --git a/lib/ui.py b/lib/ui.py index 4716c6e..5039e16 100644 --- a/lib/ui.py +++ b/lib/ui.py @@ -20,6 +20,7 @@ class UpdaterWindow(Adw.ApplicationWindow): self.set_resizable(True) self._pending_rows: list[Adw.ActionRow] = [] + self._aur_rows: list[Adw.ActionRow] = [] self._recent_rows: list[Adw.ActionRow] = [] self._build_ui() @@ -68,6 +69,21 @@ class UpdaterWindow(Adw.ApplicationWindow): main_box.append(self.pending_group) + # Groupe "Mises à jour AUR" + self.aur_group = Adw.PreferencesGroup() + self.aur_group.set_title("Mises à jour AUR") + self.aur_group.set_description( + "Ces paquets seront installés automatiquement lors de la prochaine session" + ) + + self.install_aur_btn = Gtk.Button.new_with_label("Installer maintenant") + self.install_aur_btn.add_css_class("suggested-action") + self.install_aur_btn.add_css_class("pill") + self.install_aur_btn.connect("clicked", self._on_install_aur_clicked) + self.aur_group.set_header_suffix(self.install_aur_btn) + + main_box.append(self.aur_group) + # Groupe "Récemment installées" self.recent_group = Adw.PreferencesGroup() self.recent_group.set_title("Récemment installées") @@ -185,6 +201,7 @@ class UpdaterWindow(Adw.ApplicationWindow): def _update_ui(self, state: dict): status = state.get("status", "idle") pending = state.get("pending_restart", []) + pending_aur = state.get("pending_aur", []) history = state.get("installed_history", []) last_check = state.get("last_check") errors = state.get("errors", []) @@ -240,10 +257,13 @@ class UpdaterWindow(Adw.ApplicationWindow): pass self.status_sub_lbl.set_text(sub) - # ── Bouton "Installer maintenant" ──────────────────────────────── + # ── Bouton "Installer maintenant" (officiel) ───────────────────── can_install = status in ("idle", "error") and bool(pending) self.install_now_btn.set_sensitive(can_install) + # ── Bouton "Installer maintenant" (AUR) ────────────────────────── + self.install_aur_btn.set_sensitive(bool(pending_aur)) + # ── Liste des paquets en attente de redémarrage ────────────────── for row in self._pending_rows: self.pending_group.remove(row) @@ -263,6 +283,25 @@ class UpdaterWindow(Adw.ApplicationWindow): self.pending_group.set_visible(bool(pending)) + # ── Liste des paquets AUR en attente ───────────────────────────── + for row in self._aur_rows: + self.aur_group.remove(row) + self._aur_rows.clear() + + for pkg in pending_aur: + row = Adw.ActionRow() + row.set_title(GLib.markup_escape_text(pkg.get("name", ""))) + row.set_subtitle( + f"{pkg.get('old_version', '')} → {pkg.get('new_version', '')}" + ) + icon = Gtk.Image.new_from_icon_name("application-x-addon-symbolic") + icon.add_css_class("accent") + row.add_prefix(icon) + self.aur_group.add(row) + self._aur_rows.append(row) + + self.aur_group.set_visible(bool(pending_aur)) + # ── Historique des installations récentes ──────────────────────── for row in self._recent_rows: self.recent_group.remove(row) @@ -321,6 +360,10 @@ class UpdaterWindow(Adw.ApplicationWindow): self.install_now_btn.set_sensitive(False) threading.Thread(target=self._send_install_pending, daemon=True).start() + def _on_install_aur_clicked(self, _btn): + self.install_aur_btn.set_sensitive(False) + threading.Thread(target=self._send_install_aur, daemon=True).start() + def _send_install_pending(self): try: s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM) @@ -342,6 +385,27 @@ class UpdaterWindow(Adw.ApplicationWindow): ) GLib.idle_add(self.install_now_btn.set_sensitive, True) + def _send_install_aur(self): + try: + s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM) + s.settimeout(5) + s.connect(str(SOCKET_PATH)) + s.sendall(json.dumps({"action": "install_aur"}).encode()) + s.recv(1024) + s.close() + except PermissionError: + GLib.idle_add( + self._show_toast, + "Permission refusée — êtes-vous dans le groupe wheel ?", + ) + GLib.idle_add(self.install_aur_btn.set_sensitive, True) + except Exception as e: + GLib.idle_add( + self._show_toast, + f"Impossible de contacter le service : {e}", + ) + GLib.idle_add(self.install_aur_btn.set_sensitive, True) + def _send_check_now(self): try: s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM)