fix: fix Aur detection packages
This commit is contained in:
110
lib/aur_manager.py
Normal file
110
lib/aur_manager.py
Normal file
@ -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))
|
||||||
@ -5,6 +5,10 @@ import os
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import state as state_mod
|
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 classifier import classify
|
||||||
from config import FIRST_RUN_DELAY_SECONDS, SOCKET_PATH, UPDATE_INTERVAL_SECONDS
|
from config import FIRST_RUN_DELAY_SECONDS, SOCKET_PATH, UPDATE_INTERVAL_SECONDS
|
||||||
from package_manager import (
|
from package_manager import (
|
||||||
@ -37,6 +41,8 @@ class Daemon:
|
|||||||
logger.info("pacman est verrouillé (autre processus actif), cycle ignoré")
|
logger.info("pacman est verrouillé (autre processus actif), cycle ignoré")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
asyncio.create_task(self.aur_cycle())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._set_status("checking")
|
self._set_status("checking")
|
||||||
logger.info("Vérification des mises à jour disponibles…")
|
logger.info("Vérification des mises à jour disponibles…")
|
||||||
@ -125,6 +131,56 @@ class Daemon:
|
|||||||
self._add_error(str(e))
|
self._add_error(str(e))
|
||||||
self._set_status("error")
|
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):
|
async def install_pending_cycle(self):
|
||||||
if self._state["status"] not in ("idle", "error"):
|
if self._state["status"] not in ("idle", "error"):
|
||||||
logger.info("Une opération est déjà en cours, installation ignorée")
|
logger.info("Une opération est déjà en cours, installation ignorée")
|
||||||
@ -190,6 +246,9 @@ class Daemon:
|
|||||||
elif action == "install_pending":
|
elif action == "install_pending":
|
||||||
asyncio.create_task(self.install_pending_cycle())
|
asyncio.create_task(self.install_pending_cycle())
|
||||||
response = {"status": "ok", "message": "Installation lancée"}
|
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":
|
elif action == "get_state":
|
||||||
response = {"status": "ok", "state": self._state}
|
response = {"status": "ok", "state": self._state}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -12,6 +12,7 @@ _DEFAULTS = {
|
|||||||
"last_check": None,
|
"last_check": None,
|
||||||
"available_updates": [],
|
"available_updates": [],
|
||||||
"pending_restart": [],
|
"pending_restart": [],
|
||||||
|
"pending_aur": [],
|
||||||
"installed_history": [],
|
"installed_history": [],
|
||||||
"errors": [],
|
"errors": [],
|
||||||
}
|
}
|
||||||
|
|||||||
66
lib/ui.py
66
lib/ui.py
@ -20,6 +20,7 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
self.set_resizable(True)
|
self.set_resizable(True)
|
||||||
|
|
||||||
self._pending_rows: list[Adw.ActionRow] = []
|
self._pending_rows: list[Adw.ActionRow] = []
|
||||||
|
self._aur_rows: list[Adw.ActionRow] = []
|
||||||
self._recent_rows: list[Adw.ActionRow] = []
|
self._recent_rows: list[Adw.ActionRow] = []
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
@ -68,6 +69,21 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
main_box.append(self.pending_group)
|
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"
|
# Groupe "Récemment installées"
|
||||||
self.recent_group = Adw.PreferencesGroup()
|
self.recent_group = Adw.PreferencesGroup()
|
||||||
self.recent_group.set_title("Récemment installées")
|
self.recent_group.set_title("Récemment installées")
|
||||||
@ -185,6 +201,7 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
def _update_ui(self, state: dict):
|
def _update_ui(self, state: dict):
|
||||||
status = state.get("status", "idle")
|
status = state.get("status", "idle")
|
||||||
pending = state.get("pending_restart", [])
|
pending = state.get("pending_restart", [])
|
||||||
|
pending_aur = state.get("pending_aur", [])
|
||||||
history = state.get("installed_history", [])
|
history = state.get("installed_history", [])
|
||||||
last_check = state.get("last_check")
|
last_check = state.get("last_check")
|
||||||
errors = state.get("errors", [])
|
errors = state.get("errors", [])
|
||||||
@ -240,10 +257,13 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
pass
|
pass
|
||||||
self.status_sub_lbl.set_text(sub)
|
self.status_sub_lbl.set_text(sub)
|
||||||
|
|
||||||
# ── Bouton "Installer maintenant" ────────────────────────────────
|
# ── Bouton "Installer maintenant" (officiel) ─────────────────────
|
||||||
can_install = status in ("idle", "error") and bool(pending)
|
can_install = status in ("idle", "error") and bool(pending)
|
||||||
self.install_now_btn.set_sensitive(can_install)
|
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 ──────────────────
|
# ── Liste des paquets en attente de redémarrage ──────────────────
|
||||||
for row in self._pending_rows:
|
for row in self._pending_rows:
|
||||||
self.pending_group.remove(row)
|
self.pending_group.remove(row)
|
||||||
@ -263,6 +283,25 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
self.pending_group.set_visible(bool(pending))
|
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 ────────────────────────
|
# ── Historique des installations récentes ────────────────────────
|
||||||
for row in self._recent_rows:
|
for row in self._recent_rows:
|
||||||
self.recent_group.remove(row)
|
self.recent_group.remove(row)
|
||||||
@ -321,6 +360,10 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
self.install_now_btn.set_sensitive(False)
|
self.install_now_btn.set_sensitive(False)
|
||||||
threading.Thread(target=self._send_install_pending, daemon=True).start()
|
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):
|
def _send_install_pending(self):
|
||||||
try:
|
try:
|
||||||
s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM)
|
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)
|
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):
|
def _send_check_now(self):
|
||||||
try:
|
try:
|
||||||
s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM)
|
s = sock_module.socket(sock_module.AF_UNIX, sock_module.SOCK_STREAM)
|
||||||
|
|||||||
Reference in New Issue
Block a user