feat: add feature to update cachyOS in the background
This commit is contained in:
192
lib/daemon.py
Normal file
192
lib/daemon.py
Normal 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()
|
||||
Reference in New Issue
Block a user