feat: add feature to update cachyOS in the background

This commit is contained in:
2026-05-05 08:30:33 +02:00
parent 4a0ae14f8b
commit 3ea6ce58b3
15 changed files with 1190 additions and 0 deletions

192
lib/daemon.py Normal file
View File

@ -0,0 +1,192 @@
import asyncio
import json
import logging
import os
from datetime import datetime
import state as state_mod
from classifier import classify
from config import FIRST_RUN_DELAY_SECONDS, SOCKET_PATH, UPDATE_INTERVAL_SECONDS
from package_manager import (
check_updates, download_packages, install_packages,
is_pacman_locked, sync_db,
)
logger = logging.getLogger(__name__)
class Daemon:
def __init__(self):
self._state = state_mod.load()
self._state["status"] = "idle"
self._running = True
def _save(self):
state_mod.save(self._state)
def _set_status(self, status: str):
self._state["status"] = status
self._save()
async def update_cycle(self):
if self._state["status"] not in ("idle", "error"):
logger.info("Mise à jour déjà en cours, cycle ignoré")
return
if is_pacman_locked():
logger.info("pacman est verrouillé (autre processus actif), cycle ignoré")
return
try:
self._set_status("checking")
logger.info("Vérification des mises à jour disponibles…")
packages = await asyncio.to_thread(check_updates)
self._state["last_check"] = datetime.now().isoformat()
if not packages:
logger.info("Aucune mise à jour disponible")
self._state["available_updates"] = []
self._set_status("idle")
return
logger.info(f"{len(packages)} mise(s) à jour trouvée(s)")
self._state["available_updates"] = [
{"name": p.name, "old_version": p.old_version, "new_version": p.new_version}
for p in packages
]
safe, deferred = classify(packages)
logger.info(f"{len(safe)} immédiate(s), {len(deferred)} reportée(s)")
# Ajouter les paquets reportés à la liste d'attente (sans doublons)
existing = {p["name"] for p in self._state["pending_restart"]}
for pkg in deferred:
if pkg.name not in existing:
self._state["pending_restart"].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()
# Pré-télécharger tous les paquets reportés pendant que le réseau est disponible
pending_names = [p["name"] for p in self._state["pending_restart"]]
if pending_names:
logger.info(f"Pré-téléchargement des paquets reportés : {', '.join(pending_names)}")
ok = await asyncio.to_thread(download_packages, pending_names)
if not ok:
logger.warning("Pré-téléchargement partiel — une nouvelle tentative aura lieu au prochain cycle")
if not safe:
logger.info("Aucun paquet à installer immédiatement")
self._set_status("idle")
return
# Synchroniser la DB et télécharger
self._set_status("downloading")
safe_names = [p.name for p in safe]
logger.info(f"Téléchargement : {', '.join(safe_names)}")
ok = await asyncio.to_thread(download_packages, safe_names)
if not ok:
self._add_error("Échec du téléchargement des paquets")
self._set_status("error")
return
# Installer les paquets sûrs
self._set_status("installing")
logger.info(f"Installation : {', '.join(safe_names)}")
result = await asyncio.to_thread(install_packages, safe_names)
if result.success:
logger.info("Installation réussie")
now = datetime.now().isoformat()
for pkg in safe:
self._state["installed_history"].append({
"name": pkg.name,
"old_version": pkg.old_version,
"new_version": pkg.new_version,
"installed_at": now,
})
self._state["installed_history"] = self._state["installed_history"][-200:]
installed_set = set(result.installed)
self._state["available_updates"] = [
u for u in self._state["available_updates"]
if u["name"] not in installed_set
]
else:
logger.error(f"Installation échouée : {result.error}")
self._add_error(f"Échec de l'installation : {result.error}")
self._set_status("idle")
except Exception as e:
logger.exception(f"Cycle de mise à jour interrompu : {e}")
self._add_error(str(e))
self._set_status("error")
def _add_error(self, message: str):
self._state["errors"].append({
"time": datetime.now().isoformat(),
"message": message,
})
self._state["errors"] = self._state["errors"][-20:]
self._save()
async def _handle_client(self, reader, writer):
try:
data = await asyncio.wait_for(reader.read(4096), timeout=5)
cmd = json.loads(data.decode())
action = cmd.get("action")
if action == "check_now":
asyncio.create_task(self.update_cycle())
response = {"status": "ok", "message": "Vérification lancée"}
elif action == "get_state":
response = {"status": "ok", "state": self._state}
else:
response = {"status": "error", "message": "Commande inconnue"}
writer.write(json.dumps(response, default=str).encode())
await writer.drain()
except Exception as e:
logger.debug(f"Erreur client : {e}")
finally:
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def run(self):
if SOCKET_PATH.exists():
SOCKET_PATH.unlink()
SOCKET_PATH.parent.mkdir(parents=True, exist_ok=True)
server = await asyncio.start_unix_server(
self._handle_client, path=str(SOCKET_PATH)
)
# Permettre aux membres du groupe wheel de se connecter
os.chmod(SOCKET_PATH, 0o660)
try:
import grp
gid = grp.getgrnam("wheel").gr_gid
os.chown(SOCKET_PATH, 0, gid)
except Exception:
os.chmod(SOCKET_PATH, 0o666)
logger.info("Daemon CachyOS Updater démarré")
self._save()
async with server:
await asyncio.sleep(FIRST_RUN_DELAY_SECONDS)
await self.update_cycle()
while self._running:
await asyncio.sleep(UPDATE_INTERVAL_SECONDS)
await self.update_cycle()