feat: add feature to update cachyOS in the background
This commit is contained in:
26
bin/cachyos-updater
Executable file
26
bin/cachyos-updater
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Point d'entrée du daemon CachyOS Updater (doit tourner en root)."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
if os.geteuid() != 0:
|
||||
print("Erreur : ce daemon doit être lancé en tant que root.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.path.insert(0, "/usr/lib/cachyos-updater")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
|
||||
from daemon import Daemon
|
||||
|
||||
try:
|
||||
asyncio.run(Daemon().run())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
15
bin/cachyos-updater-shutdown
Executable file
15
bin/cachyos-updater-shutdown
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Installateur de mises à jour au moment de l'extinction (doit tourner en root)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if os.geteuid() != 0:
|
||||
print("Erreur : doit être lancé en tant que root.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.path.insert(0, "/usr/lib/cachyos-updater")
|
||||
|
||||
import shutdown_installer
|
||||
|
||||
shutdown_installer.run()
|
||||
11
bin/cachyos-updater-ui
Executable file
11
bin/cachyos-updater-ui
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interface graphique CachyOS Updater."""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/usr/lib/cachyos-updater")
|
||||
|
||||
from ui import UpdaterApp
|
||||
|
||||
app = UpdaterApp()
|
||||
sys.exit(app.run(sys.argv))
|
||||
12
data/cachyos-updater.desktop
Normal file
12
data/cachyos-updater.desktop
Normal file
@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Name=Mises à jour
|
||||
Name[en]=System Updates
|
||||
Comment=Gérer les mises à jour du système CachyOS
|
||||
Comment[en]=Manage CachyOS system updates
|
||||
Exec=cachyos-updater-ui
|
||||
Icon=software-update-available
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=System;PackageManager;
|
||||
Keywords=mise à jour;update;pacman;système;
|
||||
StartupNotify=true
|
||||
90
install.sh
Executable file
90
install.sh
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Script d'installation de CachyOS Updater
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[✓]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error "Ce script doit être lancé en tant que root (sudo ./install.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Vérification des dépendances ───────────────────────────────────────────
|
||||
info "Vérification des dépendances…"
|
||||
|
||||
deps=(python python-gobject gtk4 libadwaita pacman-contrib)
|
||||
missing=()
|
||||
for dep in "${deps[@]}"; do
|
||||
if ! pacman -Qi "$dep" &>/dev/null; then
|
||||
missing+=("$dep")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
warn "Paquets manquants : ${missing[*]}"
|
||||
info "Installation des dépendances…"
|
||||
pacman -S --noconfirm --needed "${missing[@]}"
|
||||
fi
|
||||
|
||||
# ── Installation des fichiers ──────────────────────────────────────────────
|
||||
info "Installation des fichiers…"
|
||||
|
||||
# Bibliothèques Python
|
||||
install -d /usr/lib/cachyos-updater
|
||||
install -m 644 "$SCRIPT_DIR"/lib/*.py /usr/lib/cachyos-updater/
|
||||
|
||||
# Exécutables
|
||||
install -m 755 "$SCRIPT_DIR"/bin/cachyos-updater /usr/bin/cachyos-updater
|
||||
install -m 755 "$SCRIPT_DIR"/bin/cachyos-updater-ui /usr/bin/cachyos-updater-ui
|
||||
install -m 755 "$SCRIPT_DIR"/bin/cachyos-updater-shutdown /usr/bin/cachyos-updater-shutdown
|
||||
|
||||
# Unités systemd
|
||||
install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater.service \
|
||||
/usr/lib/systemd/system/cachyos-updater.service
|
||||
install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater.timer \
|
||||
/usr/lib/systemd/system/cachyos-updater.timer
|
||||
install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater-shutdown.service \
|
||||
/usr/lib/systemd/system/cachyos-updater-shutdown.service
|
||||
|
||||
# Fichier .desktop
|
||||
install -m 644 "$SCRIPT_DIR"/data/cachyos-updater.desktop \
|
||||
/usr/share/applications/cachyos-updater.desktop
|
||||
|
||||
# Répertoire de données
|
||||
install -d -m 755 /var/lib/cachyos-updater
|
||||
|
||||
# ── Activation des services ────────────────────────────────────────────────
|
||||
info "Configuration des services systemd…"
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Activer le timer (démarrage automatique au boot)
|
||||
systemctl enable --now cachyos-updater.timer
|
||||
info "Timer activé : vérification au démarrage puis toutes les heures"
|
||||
|
||||
# Activer le service de shutdown (installation avant extinction)
|
||||
systemctl enable cachyos-updater-shutdown.service
|
||||
info "Service shutdown activé : installation des mises à jour reportées à l'extinction"
|
||||
|
||||
# ── Résumé ─────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}Installation terminée !${NC}"
|
||||
echo ""
|
||||
echo " • Les mises à jour sont vérifiées automatiquement toutes les heures"
|
||||
echo " • Les paquets sûrs sont installés en arrière-plan sans interruption"
|
||||
echo " • Les mises à jour nécessitant un redémarrage (kernel, systemd…)"
|
||||
echo " seront installées automatiquement à la prochaine extinction du PC"
|
||||
echo ""
|
||||
echo " Interface graphique : cachyos-updater-ui"
|
||||
echo " Journaux daemon : journalctl -u cachyos-updater -f"
|
||||
echo " Journaux shutdown : journalctl -u cachyos-updater-shutdown"
|
||||
echo ""
|
||||
22
lib/classifier.py
Normal file
22
lib/classifier.py
Normal file
@ -0,0 +1,22 @@
|
||||
import re
|
||||
from config import PACKAGES_RESTART_REQUIRED, PACKAGES_SERVICE_RESTART, PACKAGES_RESTART_PATTERNS
|
||||
|
||||
|
||||
def needs_restart(name: str) -> bool:
|
||||
if name in PACKAGES_RESTART_REQUIRED:
|
||||
return True
|
||||
if name in PACKAGES_SERVICE_RESTART:
|
||||
return True
|
||||
return any(re.match(p, name) for p in PACKAGES_RESTART_PATTERNS)
|
||||
|
||||
|
||||
def classify(packages) -> tuple[list, list]:
|
||||
"""Retourne (sûrs, reportés).
|
||||
|
||||
sûrs : installables immédiatement, sans aucun redémarrage
|
||||
reportés: nécessitent un redémarrage service ou système → installation à l'extinction
|
||||
"""
|
||||
safe, deferred = [], []
|
||||
for pkg in packages:
|
||||
(deferred if needs_restart(pkg.name) else safe).append(pkg)
|
||||
return safe, deferred
|
||||
53
lib/config.py
Normal file
53
lib/config.py
Normal file
@ -0,0 +1,53 @@
|
||||
from pathlib import Path
|
||||
|
||||
STATE_FILE = Path("/var/lib/cachyos-updater/state.json")
|
||||
LOG_FILE = Path("/var/log/cachyos-updater.log")
|
||||
SOCKET_PATH = Path("/run/cachyos-updater.sock")
|
||||
|
||||
# Paquets nécessitant un redémarrage complet du système
|
||||
PACKAGES_RESTART_REQUIRED = {
|
||||
"linux", "linux-lts", "linux-zen", "linux-hardened",
|
||||
"linux-cachyos", "linux-cachyos-lts", "linux-cachyos-rc",
|
||||
"linux-cachyos-bore", "linux-cachyos-bore-lts",
|
||||
"systemd", "systemd-libs", "systemd-sysvcompat", "systemd-resolvconf",
|
||||
"glibc", "lib32-glibc",
|
||||
"linux-firmware", "linux-firmware-whence",
|
||||
"mkinitcpio", "mkinitcpio-busybox",
|
||||
"grub", "efibootmgr", "refind",
|
||||
"dracut",
|
||||
}
|
||||
|
||||
# Paquets nécessitant le redémarrage de services (aussi reportés à l'extinction)
|
||||
PACKAGES_SERVICE_RESTART = {
|
||||
"pipewire", "pipewire-audio", "pipewire-alsa", "pipewire-jack", "pipewire-pulse",
|
||||
"wireplumber", "pipewire-wireplumber",
|
||||
"networkmanager", "network-manager-applet",
|
||||
"bluez", "bluez-utils", "bluez-libs",
|
||||
"cups", "cups-filters",
|
||||
"avahi",
|
||||
"dbus", "dbus-broker", "dbus-broker-units",
|
||||
"polkit",
|
||||
"udisks2",
|
||||
"upower",
|
||||
"mesa", "lib32-mesa",
|
||||
"vulkan-radeon", "vulkan-intel", "vulkan-nouveau",
|
||||
"lib32-vulkan-radeon", "lib32-vulkan-intel",
|
||||
"wayland",
|
||||
"xorg-server", "xorg-server-common",
|
||||
"gdm", "sddm", "lightdm",
|
||||
"plymouth",
|
||||
}
|
||||
|
||||
# Patterns de noms de paquets nécessitant un redémarrage
|
||||
PACKAGES_RESTART_PATTERNS = [
|
||||
r"^linux-[0-9]",
|
||||
r"^linux-cachyos",
|
||||
r"^linux-headers",
|
||||
r"^nvidia",
|
||||
r"^cuda",
|
||||
r"^amdgpu-pro",
|
||||
r"^rocm-",
|
||||
]
|
||||
|
||||
UPDATE_INTERVAL_SECONDS = 3600 # Vérification toutes les heures
|
||||
FIRST_RUN_DELAY_SECONDS = 0 # Pas de délai : systemd garantit le réseau via network-online.target
|
||||
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()
|
||||
90
lib/package_manager.py
Normal file
90
lib/package_manager.py
Normal file
@ -0,0 +1,90 @@
|
||||
import subprocess
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PACMAN_LOCK = Path("/var/lib/pacman/db.lck")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
name: str
|
||||
old_version: str
|
||||
new_version: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallResult:
|
||||
success: bool
|
||||
installed: list = field(default_factory=list)
|
||||
failed: list = field(default_factory=list)
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def is_pacman_locked() -> bool:
|
||||
return PACMAN_LOCK.exists()
|
||||
|
||||
|
||||
def check_updates() -> list[Package]:
|
||||
"""Vérifie les mises à jour disponibles via checkupdates (sans droits root)."""
|
||||
result = subprocess.run(
|
||||
["checkupdates"],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
packages = []
|
||||
for line in result.stdout.strip().splitlines():
|
||||
parts = line.split()
|
||||
# Format attendu : "nom ancienne_version -> nouvelle_version"
|
||||
if len(parts) >= 4 and parts[2] == "->":
|
||||
packages.append(Package(
|
||||
name=parts[0],
|
||||
old_version=parts[1],
|
||||
new_version=parts[3],
|
||||
))
|
||||
return packages
|
||||
|
||||
|
||||
def download_packages(names: list[str]) -> bool:
|
||||
"""Télécharge les paquets dans le cache sans les installer."""
|
||||
if not names:
|
||||
return True
|
||||
result = subprocess.run(
|
||||
["pacman", "-Syw", "--noconfirm", "--noprogressbar"] + names,
|
||||
capture_output=True, text=True, timeout=600
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Téléchargement échoué : {result.stderr[-500:]}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def sync_db() -> bool:
|
||||
"""Synchronise la base de données des paquets."""
|
||||
result = subprocess.run(
|
||||
["pacman", "-Sy", "--noconfirm"],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def install_packages(names: list[str]) -> InstallResult:
|
||||
"""Installe des paquets spécifiques depuis le cache."""
|
||||
if not names:
|
||||
return InstallResult(success=True)
|
||||
|
||||
result = subprocess.run(
|
||||
["pacman", "-S", "--noconfirm", "--needed", "--noprogressbar"] + names,
|
||||
capture_output=True, text=True, timeout=600
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return InstallResult(success=True, installed=names)
|
||||
else:
|
||||
return InstallResult(
|
||||
success=False,
|
||||
failed=names,
|
||||
error=result.stderr[-800:],
|
||||
)
|
||||
237
lib/shutdown_installer.py
Normal file
237
lib/shutdown_installer.py
Normal file
@ -0,0 +1,237 @@
|
||||
"""Installateur exécuté juste avant l'extinction du PC.
|
||||
|
||||
Installe les paquets qui nécessitaient un redémarrage (kernel, systemd, etc.)
|
||||
puis laisse systemd terminer la séquence d'arrêt normalement.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import state as state_mod
|
||||
from package_manager import install_packages, is_pacman_locked
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [SHUTDOWN] %(message)s",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Écran de progression ────────────────────────────────────────────────────
|
||||
|
||||
CYAN = "\033[1;36m"
|
||||
GREEN = "\033[1;32m"
|
||||
RED = "\033[1;31m"
|
||||
YELLOW = "\033[1;33m"
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
CLEAR = "\033[2J\033[H" # Efface l'écran, curseur en haut à gauche
|
||||
HIDE_CURSOR = "\033[?25l"
|
||||
SHOW_CURSOR = "\033[?25h"
|
||||
|
||||
SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
|
||||
|
||||
class ProgressDisplay:
|
||||
"""Affiche un écran de progression pendant l'installation des mises à jour.
|
||||
|
||||
Utilise Plymouth s'il est actif (CachyOS avec splash activé),
|
||||
sinon prend le contrôle du TTY1 directement.
|
||||
"""
|
||||
|
||||
def __init__(self, packages: list[dict]):
|
||||
self._packages = packages
|
||||
self._active = False
|
||||
self._status = "Installation en cours…"
|
||||
self._spin_thread: threading.Thread | None = None
|
||||
self._tty_fd: int | None = None
|
||||
self._use_plymouth = self._probe_plymouth()
|
||||
|
||||
def _probe_plymouth(self) -> bool:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["plymouth", "--ping"],
|
||||
capture_output=True, timeout=2
|
||||
)
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── Interface publique ──────────────────────────────────────────────────
|
||||
|
||||
def start(self):
|
||||
self._active = True
|
||||
if self._use_plymouth:
|
||||
self._plymouth_show()
|
||||
else:
|
||||
self._tty_start()
|
||||
|
||||
def update(self, message: str):
|
||||
self._status = message
|
||||
if self._use_plymouth:
|
||||
self._plym_msg(f"⟳ {message}")
|
||||
|
||||
def finish(self, success: bool):
|
||||
self._active = False
|
||||
if success:
|
||||
final = f"✓ Mises à jour installées avec succès"
|
||||
color = GREEN
|
||||
else:
|
||||
final = f"✗ Échec de l'installation — voir les journaux"
|
||||
color = RED
|
||||
|
||||
if self._use_plymouth:
|
||||
self._plym_msg(final)
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
if self._spin_thread:
|
||||
self._spin_thread.join(timeout=1)
|
||||
self._tty_finish(final, color)
|
||||
time.sleep(2)
|
||||
self._tty_close()
|
||||
|
||||
# ── Plymouth ────────────────────────────────────────────────────────────
|
||||
|
||||
def _plymouth_show(self):
|
||||
names = ", ".join(p["name"] for p in self._packages[:5])
|
||||
extra = f" (+{len(self._packages) - 5})" if len(self._packages) > 5 else ""
|
||||
self._plym_msg(f"⟳ Installation des mises à jour… {names}{extra}")
|
||||
|
||||
def _plym_msg(self, text: str):
|
||||
try:
|
||||
subprocess.run(
|
||||
["plymouth", "message", f"--text={text}"],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── TTY direct ─────────────────────────────────────────────────────────
|
||||
|
||||
def _tty_start(self):
|
||||
try:
|
||||
subprocess.run(["chvt", "1"], capture_output=True, timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for path in ("/dev/tty1", "/dev/console"):
|
||||
try:
|
||||
self._tty_fd = os.open(path, os.O_WRONLY | os.O_NOCTTY)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self._tty_write(self._build_screen())
|
||||
self._spin_thread = threading.Thread(target=self._spin_loop, daemon=True)
|
||||
self._spin_thread.start()
|
||||
|
||||
def _build_screen(self) -> str:
|
||||
n = len(self._packages)
|
||||
pkg_lines = ""
|
||||
for pkg in self._packages[:10]:
|
||||
name = pkg["name"]
|
||||
old_v = pkg.get("old_version", "")
|
||||
new_v = pkg.get("new_version", "")
|
||||
pkg_lines += f" {CYAN}•{RESET} {BOLD}{name:<30}{RESET} {old_v} → {new_v}\n"
|
||||
if n > 10:
|
||||
pkg_lines += f" {CYAN}•{RESET} … et {n - 10} autre(s) paquet(s)\n"
|
||||
|
||||
return (
|
||||
f"{CLEAR}{HIDE_CURSOR}"
|
||||
f"\n\n\n"
|
||||
f" {CYAN}{'─' * 60}{RESET}\n"
|
||||
f"\n"
|
||||
f" {BOLD}CachyOS Updater{RESET}\n"
|
||||
f"\n"
|
||||
f" Installation de {BOLD}{n} mise{'s' if n > 1 else ''} à jour{RESET} "
|
||||
f"avant extinction…\n"
|
||||
f"\n"
|
||||
f"{pkg_lines}"
|
||||
f"\n"
|
||||
f" {YELLOW}Ne pas éteindre ou débrancher votre PC.{RESET}\n"
|
||||
f"\n"
|
||||
f" {CYAN}{'─' * 60}{RESET}\n"
|
||||
f"\n"
|
||||
f" " # Point de départ du spinner (sera écrasé)
|
||||
)
|
||||
|
||||
def _spin_loop(self):
|
||||
i = 0
|
||||
while self._active:
|
||||
c = SPINNER_FRAMES[i % len(SPINNER_FRAMES)]
|
||||
self._tty_write(f"\r {CYAN}{c}{RESET} {self._status}\033[K")
|
||||
i += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
def _tty_finish(self, message: str, color: str):
|
||||
self._tty_write(f"\r {color}{message}{RESET}\033[K\n")
|
||||
|
||||
def _tty_close(self):
|
||||
if self._tty_fd is not None:
|
||||
self._tty_write(SHOW_CURSOR)
|
||||
os.close(self._tty_fd)
|
||||
self._tty_fd = None
|
||||
|
||||
def _tty_write(self, text: str):
|
||||
if self._tty_fd is not None:
|
||||
try:
|
||||
os.write(self._tty_fd, text.encode("utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Point d'entrée ──────────────────────────────────────────────────────────
|
||||
|
||||
def run():
|
||||
state = state_mod.load()
|
||||
pending = state.get("pending_restart", [])
|
||||
|
||||
if not pending:
|
||||
logger.info("Aucune mise à jour en attente — extinction normale")
|
||||
sys.exit(0)
|
||||
|
||||
names = [p["name"] for p in pending]
|
||||
logger.info(
|
||||
f"Installation de {len(names)} paquet(s) avant extinction : {', '.join(names)}"
|
||||
)
|
||||
|
||||
if is_pacman_locked():
|
||||
logger.error("pacman est verrouillé, impossible d'installer les mises à jour")
|
||||
sys.exit(1)
|
||||
|
||||
display = ProgressDisplay(pending)
|
||||
display.start()
|
||||
|
||||
try:
|
||||
display.update(f"Installation de {len(names)} paquet(s)…")
|
||||
result = install_packages(names)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Erreur inattendue : {e}")
|
||||
display.finish(success=False)
|
||||
sys.exit(1)
|
||||
|
||||
if result.success:
|
||||
logger.info("Mises à jour installées avec succès avant extinction")
|
||||
now = datetime.now().isoformat()
|
||||
for pkg in pending:
|
||||
state["installed_history"].append({**pkg, "installed_at": now})
|
||||
state["installed_history"] = state["installed_history"][-200:]
|
||||
state["pending_restart"] = []
|
||||
installed_set = set(names)
|
||||
state["available_updates"] = [
|
||||
u for u in state.get("available_updates", [])
|
||||
if u["name"] not in installed_set
|
||||
]
|
||||
state_mod.save(state)
|
||||
display.finish(success=True)
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.error(f"Échec de l'installation : {result.error}")
|
||||
display.finish(success=False)
|
||||
sys.exit(1)
|
||||
43
lib/state.py
Normal file
43
lib/state.py
Normal file
@ -0,0 +1,43 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from config import STATE_FILE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULTS = {
|
||||
"status": "idle",
|
||||
"last_check": None,
|
||||
"available_updates": [],
|
||||
"pending_restart": [],
|
||||
"installed_history": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
if not STATE_FILE.exists():
|
||||
return dict(_DEFAULTS)
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
data = json.load(f)
|
||||
for k, v in _DEFAULTS.items():
|
||||
data.setdefault(k, v)
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Impossible de charger l'état : {e}")
|
||||
return dict(_DEFAULTS)
|
||||
|
||||
|
||||
def save(state: dict) -> None:
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
tmp = STATE_FILE.with_suffix(".tmp")
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(state, f, indent=2, default=str)
|
||||
tmp.replace(STATE_FILE)
|
||||
os.chmod(STATE_FILE, 0o644) # Lisible par tous pour l'interface utilisateur
|
||||
except Exception as e:
|
||||
logger.error(f"Impossible de sauvegarder l'état : {e}")
|
||||
344
lib/ui.py
Normal file
344
lib/ui.py
Normal file
@ -0,0 +1,344 @@
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Adw", "1")
|
||||
from gi.repository import Adw, Gio, GLib, Gtk
|
||||
|
||||
import json
|
||||
import socket as sock_module
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from config import SOCKET_PATH, STATE_FILE
|
||||
|
||||
|
||||
class UpdaterWindow(Adw.ApplicationWindow):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_title("Mises à jour")
|
||||
self.set_default_size(500, -1)
|
||||
self.set_resizable(True)
|
||||
|
||||
self._pending_rows: list[Adw.ActionRow] = []
|
||||
self._recent_rows: list[Adw.ActionRow] = []
|
||||
|
||||
self._build_ui()
|
||||
self._setup_file_monitor()
|
||||
self._load_state()
|
||||
|
||||
# ─── Construction de l'interface ────────────────────────────────────────
|
||||
|
||||
def _build_ui(self):
|
||||
self.toast_overlay = Adw.ToastOverlay()
|
||||
|
||||
toolbar_view = Adw.ToolbarView()
|
||||
|
||||
header = Adw.HeaderBar()
|
||||
self.refresh_btn = Gtk.Button.new_from_icon_name("view-refresh-symbolic")
|
||||
self.refresh_btn.set_tooltip_text("Vérifier maintenant")
|
||||
self.refresh_btn.connect("clicked", self._on_refresh_clicked)
|
||||
header.pack_end(self.refresh_btn)
|
||||
toolbar_view.add_top_bar(header)
|
||||
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroll.set_propagate_natural_height(True)
|
||||
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
|
||||
main_box.set_margin_top(20)
|
||||
main_box.set_margin_bottom(28)
|
||||
main_box.set_margin_start(20)
|
||||
main_box.set_margin_end(20)
|
||||
|
||||
# Carte de statut principal
|
||||
self._build_status_card(main_box)
|
||||
|
||||
# Groupe "En attente du redémarrage"
|
||||
self.pending_group = Adw.PreferencesGroup()
|
||||
self.pending_group.set_title("En attente du redémarrage")
|
||||
self.pending_group.set_description(
|
||||
"Ces paquets seront installés automatiquement à la prochaine extinction du PC"
|
||||
)
|
||||
main_box.append(self.pending_group)
|
||||
|
||||
# Groupe "Récemment installées"
|
||||
self.recent_group = Adw.PreferencesGroup()
|
||||
self.recent_group.set_title("Récemment installées")
|
||||
main_box.append(self.recent_group)
|
||||
|
||||
scroll.set_child(main_box)
|
||||
toolbar_view.set_content(scroll)
|
||||
self.toast_overlay.set_child(toolbar_view)
|
||||
self.set_content(self.toast_overlay)
|
||||
|
||||
def _build_status_card(self, parent: Gtk.Box):
|
||||
card = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
card.add_css_class("card")
|
||||
|
||||
# Côté gauche : icône ou spinner
|
||||
left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
left.set_valign(Gtk.Align.CENTER)
|
||||
left.set_margin_start(20)
|
||||
left.set_margin_end(16)
|
||||
left.set_margin_top(20)
|
||||
left.set_margin_bottom(20)
|
||||
|
||||
self.status_icon = Gtk.Image()
|
||||
self.status_icon.set_pixel_size(40)
|
||||
self.status_icon.set_valign(Gtk.Align.CENTER)
|
||||
|
||||
self.status_spinner = Gtk.Spinner()
|
||||
self.status_spinner.set_size_request(40, 40)
|
||||
self.status_spinner.set_valign(Gtk.Align.CENTER)
|
||||
self.status_spinner.set_visible(False)
|
||||
|
||||
left.append(self.status_icon)
|
||||
left.append(self.status_spinner)
|
||||
|
||||
# Côté droit : texte
|
||||
text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
text_box.set_valign(Gtk.Align.CENTER)
|
||||
text_box.set_hexpand(True)
|
||||
text_box.set_margin_end(20)
|
||||
text_box.set_margin_top(18)
|
||||
text_box.set_margin_bottom(18)
|
||||
|
||||
self.status_title_lbl = Gtk.Label()
|
||||
self.status_title_lbl.set_halign(Gtk.Align.START)
|
||||
self.status_title_lbl.add_css_class("title-3")
|
||||
self.status_title_lbl.set_wrap(True)
|
||||
|
||||
self.status_sub_lbl = Gtk.Label()
|
||||
self.status_sub_lbl.set_halign(Gtk.Align.START)
|
||||
self.status_sub_lbl.add_css_class("caption")
|
||||
self.status_sub_lbl.add_css_class("dim-label")
|
||||
self.status_sub_lbl.set_wrap(True)
|
||||
|
||||
text_box.append(self.status_title_lbl)
|
||||
text_box.append(self.status_sub_lbl)
|
||||
|
||||
card.append(left)
|
||||
card.append(text_box)
|
||||
parent.append(card)
|
||||
|
||||
# ─── Surveillance du fichier d'état ────────────────────────────────────
|
||||
|
||||
def _setup_file_monitor(self):
|
||||
gfile = Gio.File.new_for_path(str(STATE_FILE))
|
||||
try:
|
||||
self._monitor = gfile.monitor_file(Gio.FileMonitorFlags.NONE, None)
|
||||
self._monitor.connect("changed", self._on_file_changed)
|
||||
except Exception:
|
||||
GLib.timeout_add_seconds(5, self._poll_state)
|
||||
|
||||
def _on_file_changed(self, _monitor, _file, _other, event):
|
||||
if event in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED):
|
||||
GLib.timeout_add(300, self._load_state)
|
||||
|
||||
def _poll_state(self) -> bool:
|
||||
self._load_state()
|
||||
return True
|
||||
|
||||
def _load_state(self) -> bool:
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
state = json.load(f)
|
||||
self._update_ui(state)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
self._show_service_unavailable()
|
||||
return False # Ne pas répéter si appelé via timeout_add
|
||||
|
||||
# ─── Mise à jour de l'interface ────────────────────────────────────────
|
||||
|
||||
def _show_service_unavailable(self):
|
||||
self._set_icon("network-offline-symbolic")
|
||||
self.status_title_lbl.set_text("Service non disponible")
|
||||
self.status_sub_lbl.set_text(
|
||||
"Démarrez le service : systemctl start cachyos-updater"
|
||||
)
|
||||
self.pending_group.set_visible(False)
|
||||
self.recent_group.set_visible(False)
|
||||
self.refresh_btn.set_sensitive(False)
|
||||
|
||||
def _set_icon(self, icon_name: str, color_class: str = ""):
|
||||
self.status_spinner.stop()
|
||||
self.status_spinner.set_visible(False)
|
||||
self.status_icon.set_visible(True)
|
||||
self.status_icon.set_from_icon_name(icon_name)
|
||||
for cls in ("success", "warning", "error", "accent", "dim-label"):
|
||||
self.status_icon.remove_css_class(cls)
|
||||
if color_class:
|
||||
self.status_icon.add_css_class(color_class)
|
||||
|
||||
def _set_spinner(self):
|
||||
self.status_icon.set_visible(False)
|
||||
self.status_spinner.set_visible(True)
|
||||
self.status_spinner.start()
|
||||
|
||||
def _update_ui(self, state: dict):
|
||||
status = state.get("status", "idle")
|
||||
pending = state.get("pending_restart", [])
|
||||
history = state.get("installed_history", [])
|
||||
last_check = state.get("last_check")
|
||||
errors = state.get("errors", [])
|
||||
|
||||
# ── Carte de statut ──────────────────────────────────────────────
|
||||
if status == "checking":
|
||||
self._set_spinner()
|
||||
self.status_title_lbl.set_text("Vérification des mises à jour…")
|
||||
self.status_sub_lbl.set_text("")
|
||||
self.refresh_btn.set_sensitive(False)
|
||||
|
||||
elif status == "downloading":
|
||||
self._set_spinner()
|
||||
self.status_title_lbl.set_text("Téléchargement des mises à jour…")
|
||||
self.status_sub_lbl.set_text("Installation dans quelques instants")
|
||||
self.refresh_btn.set_sensitive(False)
|
||||
|
||||
elif status == "installing":
|
||||
self._set_spinner()
|
||||
self.status_title_lbl.set_text("Installation en cours…")
|
||||
self.status_sub_lbl.set_text("Ne pas éteindre le PC")
|
||||
self.refresh_btn.set_sensitive(False)
|
||||
|
||||
elif status == "error":
|
||||
self._set_icon("dialog-error-symbolic", "error")
|
||||
self.status_title_lbl.set_text("Erreur lors de la mise à jour")
|
||||
self.status_sub_lbl.set_text(
|
||||
errors[-1]["message"] if errors else "Erreur inconnue"
|
||||
)
|
||||
self.refresh_btn.set_sensitive(True)
|
||||
|
||||
else: # idle
|
||||
self.refresh_btn.set_sensitive(True)
|
||||
if pending:
|
||||
n = len(pending)
|
||||
self._set_icon("software-update-available-symbolic", "warning")
|
||||
self.status_title_lbl.set_text(
|
||||
f"{n} mise{'s' if n > 1 else ''} à jour "
|
||||
f"en attente de redémarrage"
|
||||
)
|
||||
self.status_sub_lbl.set_text(
|
||||
"Seront installées automatiquement à la prochaine extinction"
|
||||
)
|
||||
else:
|
||||
self._set_icon("emblem-ok-symbolic", "success")
|
||||
self.status_title_lbl.set_text("Système à jour")
|
||||
sub = ""
|
||||
if last_check:
|
||||
try:
|
||||
dt = datetime.fromisoformat(last_check)
|
||||
sub = f"Dernière vérification : {self._relative_time(dt)}"
|
||||
except Exception:
|
||||
pass
|
||||
self.status_sub_lbl.set_text(sub)
|
||||
|
||||
# ── Liste des paquets en attente de redémarrage ──────────────────
|
||||
for row in self._pending_rows:
|
||||
self.pending_group.remove(row)
|
||||
self._pending_rows.clear()
|
||||
|
||||
for pkg in pending:
|
||||
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("system-restart-symbolic")
|
||||
icon.add_css_class("warning")
|
||||
row.add_prefix(icon)
|
||||
self.pending_group.add(row)
|
||||
self._pending_rows.append(row)
|
||||
|
||||
self.pending_group.set_visible(bool(pending))
|
||||
|
||||
# ── Historique des installations récentes ────────────────────────
|
||||
for row in self._recent_rows:
|
||||
self.recent_group.remove(row)
|
||||
self._recent_rows.clear()
|
||||
|
||||
recent = list(reversed(history[-30:]))
|
||||
for pkg in recent:
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(GLib.markup_escape_text(pkg.get("name", "")))
|
||||
subtitle = (
|
||||
f"{pkg.get('old_version', '')} → {pkg.get('new_version', '')}"
|
||||
)
|
||||
installed_at = pkg.get("installed_at", "")
|
||||
if installed_at:
|
||||
try:
|
||||
dt = datetime.fromisoformat(installed_at)
|
||||
subtitle += f" • {self._relative_time(dt)}"
|
||||
except Exception:
|
||||
pass
|
||||
row.set_subtitle(subtitle)
|
||||
icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
|
||||
icon.add_css_class("success")
|
||||
row.add_prefix(icon)
|
||||
self.recent_group.add(row)
|
||||
self._recent_rows.append(row)
|
||||
|
||||
self.recent_group.set_visible(bool(recent))
|
||||
|
||||
# ─── Utilitaires ───────────────────────────────────────────────────────
|
||||
|
||||
def _relative_time(self, dt: datetime) -> str:
|
||||
now = datetime.now()
|
||||
secs = (now - dt).total_seconds()
|
||||
if secs < 60:
|
||||
return "à l'instant"
|
||||
if secs < 3600:
|
||||
m = int(secs // 60)
|
||||
return f"il y a {m} min"
|
||||
if secs < 86400:
|
||||
h = int(secs // 3600)
|
||||
return f"il y a {h}h"
|
||||
days = int(secs // 86400)
|
||||
if days == 1:
|
||||
return f"hier à {dt.strftime('%H:%M')}"
|
||||
if days < 7:
|
||||
return f"il y a {days} jours"
|
||||
return dt.strftime("%d/%m/%Y")
|
||||
|
||||
# ─── Actions utilisateur ───────────────────────────────────────────────
|
||||
|
||||
def _on_refresh_clicked(self, _btn):
|
||||
self.refresh_btn.set_sensitive(False)
|
||||
threading.Thread(target=self._send_check_now, daemon=True).start()
|
||||
|
||||
def _send_check_now(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": "check_now"}).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.refresh_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.refresh_btn.set_sensitive, True)
|
||||
|
||||
def _show_toast(self, message: str) -> bool:
|
||||
toast = Adw.Toast.new(message)
|
||||
toast.set_timeout(4)
|
||||
self.toast_overlay.add_toast(toast)
|
||||
return False
|
||||
|
||||
|
||||
class UpdaterApp(Adw.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id="org.cachyos.Updater")
|
||||
self.connect("activate", self._on_activate)
|
||||
|
||||
def _on_activate(self, app):
|
||||
win = UpdaterWindow(application=app)
|
||||
win.present()
|
||||
19
systemd/cachyos-updater-shutdown.service
Normal file
19
systemd/cachyos-updater-shutdown.service
Normal file
@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=CachyOS Updater - Installation des mises à jour reportées avant extinction
|
||||
DefaultDependencies=no
|
||||
Before=poweroff.target halt.target reboot.target shutdown.target
|
||||
# Après Plymouth pour pouvoir lui envoyer des messages
|
||||
After=local-fs.target plymouth-start.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/cachyos-updater-shutdown
|
||||
# Jusqu'à 10 minutes pour installer les mises à jour (kernel, systemd...)
|
||||
TimeoutStartSec=600
|
||||
RemainAfterExit=no
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=cachyos-updater-shutdown
|
||||
|
||||
[Install]
|
||||
WantedBy=poweroff.target halt.target reboot.target
|
||||
20
systemd/cachyos-updater.service
Normal file
20
systemd/cachyos-updater.service
Normal file
@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=CachyOS Updater - Daemon de mises à jour automatiques
|
||||
Documentation=https://github.com/cachyos/cachyos-updater
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/cachyos-updater
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=cachyos-updater
|
||||
|
||||
# Sécurité : restreindre les accès inutiles
|
||||
ProtectHome=read-only
|
||||
ProtectHostname=yes
|
||||
PrivateTmp=yes
|
||||
NoNewPrivileges=no
|
||||
16
systemd/cachyos-updater.timer
Normal file
16
systemd/cachyos-updater.timer
Normal file
@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=CachyOS Updater - Vérification périodique des mises à jour
|
||||
Requires=cachyos-updater.service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Timer]
|
||||
# Lancer dès que le réseau est disponible au démarrage
|
||||
OnBootSec=30s
|
||||
# Puis toutes les heures après la dernière exécution
|
||||
OnUnitInactiveSec=1h
|
||||
Unit=cachyos-updater.service
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user