237 lines
8.8 KiB
Python
237 lines
8.8 KiB
Python
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()
|
|
|
|
safe_names = [p.name for p in safe]
|
|
pending_names = [p["name"] for p in self._state["pending_restart"]]
|
|
|
|
# Télécharger safe + reportés en une seule transaction pour que pacman
|
|
# puisse résoudre toutes les dépendances inter-paquets correctement.
|
|
all_names = list(dict.fromkeys(safe_names + pending_names))
|
|
self._set_status("downloading")
|
|
logger.info(f"Téléchargement ({len(all_names)} paquets) : {', '.join(all_names)}")
|
|
|
|
ok = await asyncio.to_thread(download_packages, all_names)
|
|
if not ok:
|
|
self._add_error("Échec du téléchargement des paquets")
|
|
self._set_status("error")
|
|
return
|
|
|
|
if not safe:
|
|
logger.info("Aucun paquet à installer immédiatement")
|
|
self._set_status("idle")
|
|
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")
|
|
|
|
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")
|
|
return
|
|
|
|
pending = self._state.get("pending_restart", [])
|
|
if not pending:
|
|
logger.info("Aucun paquet en attente à installer")
|
|
return
|
|
|
|
if is_pacman_locked():
|
|
logger.info("pacman est verrouillé, impossible d'installer maintenant")
|
|
self._add_error("pacman est verrouillé, réessayez dans quelques instants")
|
|
return
|
|
|
|
names = [p["name"] for p in pending]
|
|
try:
|
|
self._set_status("installing")
|
|
logger.info(f"Installation manuelle : {', '.join(names)}")
|
|
|
|
result = await asyncio.to_thread(install_packages, names)
|
|
|
|
if result.success:
|
|
logger.info("Installation manuelle réussie")
|
|
now = datetime.now().isoformat()
|
|
for pkg in pending:
|
|
self._state["installed_history"].append({**pkg, "installed_at": now})
|
|
self._state["installed_history"] = self._state["installed_history"][-200:]
|
|
self._state["pending_restart"] = []
|
|
installed_set = set(names)
|
|
self._state["available_updates"] = [
|
|
u for u in self._state["available_updates"]
|
|
if u["name"] not in installed_set
|
|
]
|
|
else:
|
|
logger.error(f"Installation manuelle é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"Erreur lors de l'installation manuelle : {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 == "install_pending":
|
|
asyncio.create_task(self.install_pending_cycle())
|
|
response = {"status": "ok", "message": "Installation 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()
|