Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d0d9835be | |||
| 4ba70cefc6 |
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Virtualenvs
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Distribution / build
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# State et logs générés à l'exécution
|
||||||
|
/var/lib/cachyos-updater/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Éditeurs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/settings.local.json
|
||||||
54
README.md
54
README.md
@ -0,0 +1,54 @@
|
|||||||
|
# CachyOS Updater
|
||||||
|
|
||||||
|
Daemon de mises à jour automatiques pour CachyOS. Il vérifie les mises à jour disponibles, installe en arrière-plan celles qui sont sûres, et reporte automatiquement à l'extinction les paquets sensibles (kernel, systemd, pilotes GPU…) pour éviter tout redémarrage de service intempestif en cours d'utilisation.
|
||||||
|
|
||||||
|
## Fonctionnement
|
||||||
|
|
||||||
|
- **Vérification au démarrage** — dès que le réseau est disponible, puis toutes les heures
|
||||||
|
- **Classification automatique** — les paquets sont séparés en deux catégories :
|
||||||
|
- *Sûrs* : installés immédiatement en arrière-plan (aucune interruption)
|
||||||
|
- *Reportés* : kernel, systemd, pilotes GPU, glibc, etc. — pré-téléchargés pendant la session, puis installés à la prochaine extinction
|
||||||
|
- **Installation à l'extinction** — les paquets reportés sont installés pendant la séquence d'arrêt, sans accès réseau nécessaire (pré-téléchargés)
|
||||||
|
- **Interface graphique** — fenêtre GTK4/Libadwaita affichant les mises à jour en attente et l'historique des installations
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
- `python`
|
||||||
|
- `python-gobject`
|
||||||
|
- `gtk4`
|
||||||
|
- `libadwaita`
|
||||||
|
- `pacman-contrib` (fournit `checkupdates`)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AnthonyBtl/CachyOS-Updater
|
||||||
|
cd CachyOS-Updater
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script installe les fichiers, configure les services systemd et les active immédiatement.
|
||||||
|
|
||||||
|
## Désinstallation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now cachyos-updater.timer cachyos-updater-shutdown.service
|
||||||
|
sudo rm -f /usr/bin/cachyos-updater{,-ui,-shutdown}
|
||||||
|
sudo rm -rf /usr/lib/cachyos-updater /var/lib/cachyos-updater
|
||||||
|
sudo rm -f /usr/lib/systemd/system/cachyos-updater{,.timer,-shutdown}.service
|
||||||
|
sudo rm -f /usr/share/applications/cachyos-updater.desktop
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lancer l'interface graphique
|
||||||
|
cachyos-updater-ui
|
||||||
|
|
||||||
|
# Suivre les logs du daemon en temps réel
|
||||||
|
journalctl -u cachyos-updater -f
|
||||||
|
|
||||||
|
# Consulter les logs de la dernière extinction
|
||||||
|
journalctl -u cachyos-updater-shutdown
|
||||||
|
```
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Name[en]=System Updates
|
|||||||
Comment=Gérer les mises à jour du système CachyOS
|
Comment=Gérer les mises à jour du système CachyOS
|
||||||
Comment[en]=Manage CachyOS system updates
|
Comment[en]=Manage CachyOS system updates
|
||||||
Exec=cachyos-updater-ui
|
Exec=cachyos-updater-ui
|
||||||
Icon=software-update-available
|
Icon=cachyos-updater
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=System;PackageManager;
|
Categories=System;PackageManager;
|
||||||
|
|||||||
30
data/cachyos-updater.svg
Normal file
30
data/cachyos-updater.svg
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="a" x1="12" y1="2" x2="20" y2="13" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#00ffcc"/>
|
||||||
|
<stop offset="1" stop-color="#00ccff"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="b" x1="12" y1="22" x2="4" y2="11" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#00aa88"/>
|
||||||
|
<stop offset="1" stop-color="#00ccff" stop-opacity=".55"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="c" x1="8" y1="8" x2="16" y2="16" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#00ffcc"/>
|
||||||
|
<stop offset="1" stop-color="#020202" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Flèche du haut (gradient cyan clair → bleu) -->
|
||||||
|
<path fill="url(#a)"
|
||||||
|
d="M12 6v3l4-4-4-4v3C7.58 4 4 7.58 4 12c0 1.57.46 3.03 1.24 4.26L6.7 14.8
|
||||||
|
C6.25 13.97 6 13.01 6 12c0-3.31 2.69-6 6-6z"/>
|
||||||
|
|
||||||
|
<!-- Flèche du bas (gradient teal → cyan transparent) -->
|
||||||
|
<path fill="url(#b)"
|
||||||
|
d="M18.76 7.74 17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3
|
||||||
|
c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/>
|
||||||
|
|
||||||
|
<!-- Reflet interne (style CachyOS) -->
|
||||||
|
<path fill="url(#c)"
|
||||||
|
d="M12 6v3l2.12-2.12A5.99 5.99 0 0 0 12 6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -55,6 +55,12 @@ install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater.timer \
|
|||||||
install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater-shutdown.service \
|
install -m 644 "$SCRIPT_DIR"/systemd/cachyos-updater-shutdown.service \
|
||||||
/usr/lib/systemd/system/cachyos-updater-shutdown.service
|
/usr/lib/systemd/system/cachyos-updater-shutdown.service
|
||||||
|
|
||||||
|
# Icône
|
||||||
|
install -d /usr/share/icons/hicolor/scalable/apps
|
||||||
|
install -m 644 "$SCRIPT_DIR"/data/cachyos-updater.svg \
|
||||||
|
/usr/share/icons/hicolor/scalable/apps/cachyos-updater.svg
|
||||||
|
gtk-update-icon-cache -f -t /usr/share/icons/hicolor &>/dev/null || true
|
||||||
|
|
||||||
# Fichier .desktop
|
# Fichier .desktop
|
||||||
install -m 644 "$SCRIPT_DIR"/data/cachyos-updater.desktop \
|
install -m 644 "$SCRIPT_DIR"/data/cachyos-updater.desktop \
|
||||||
/usr/share/applications/cachyos-updater.desktop
|
/usr/share/applications/cachyos-updater.desktop
|
||||||
|
|||||||
@ -72,30 +72,26 @@ class Daemon:
|
|||||||
existing.add(pkg.name)
|
existing.add(pkg.name)
|
||||||
self._save()
|
self._save()
|
||||||
|
|
||||||
# Pré-télécharger tous les paquets reportés pendant que le réseau est disponible
|
safe_names = [p.name for p in safe]
|
||||||
pending_names = [p["name"] for p in self._state["pending_restart"]]
|
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)}")
|
# Télécharger safe + reportés en une seule transaction pour que pacman
|
||||||
ok = await asyncio.to_thread(download_packages, pending_names)
|
# 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:
|
if not ok:
|
||||||
logger.warning("Pré-téléchargement partiel — une nouvelle tentative aura lieu au prochain cycle")
|
self._add_error("Échec du téléchargement des paquets")
|
||||||
|
self._set_status("error")
|
||||||
|
return
|
||||||
|
|
||||||
if not safe:
|
if not safe:
|
||||||
logger.info("Aucun paquet à installer immédiatement")
|
logger.info("Aucun paquet à installer immédiatement")
|
||||||
self._set_status("idle")
|
self._set_status("idle")
|
||||||
return
|
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
|
# Installer les paquets sûrs
|
||||||
self._set_status("installing")
|
self._set_status("installing")
|
||||||
logger.info(f"Installation : {', '.join(safe_names)}")
|
logger.info(f"Installation : {', '.join(safe_names)}")
|
||||||
@ -129,6 +125,51 @@ class Daemon:
|
|||||||
self._add_error(str(e))
|
self._add_error(str(e))
|
||||||
self._set_status("error")
|
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):
|
def _add_error(self, message: str):
|
||||||
self._state["errors"].append({
|
self._state["errors"].append({
|
||||||
"time": datetime.now().isoformat(),
|
"time": datetime.now().isoformat(),
|
||||||
@ -146,6 +187,9 @@ class Daemon:
|
|||||||
if action == "check_now":
|
if action == "check_now":
|
||||||
asyncio.create_task(self.update_cycle())
|
asyncio.create_task(self.update_cycle())
|
||||||
response = {"status": "ok", "message": "Vérification lancée"}
|
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":
|
elif action == "get_state":
|
||||||
response = {"status": "ok", "state": self._state}
|
response = {"status": "ok", "state": self._state}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -88,3 +88,28 @@ def install_packages(names: list[str]) -> InstallResult:
|
|||||||
failed=names,
|
failed=names,
|
||||||
error=result.stderr[-800:],
|
error=result.stderr[-800:],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_cached_packages(expected_names: list[str]) -> InstallResult:
|
||||||
|
"""Installe toutes les mises à jour présentes dans le cache pacman.
|
||||||
|
|
||||||
|
Utilisé à l'extinction pour éviter les échecs de dépendances liés aux
|
||||||
|
mises à jour partielles : pacman -Su installe l'ensemble cohérent des
|
||||||
|
paquets déjà téléchargés plutôt qu'une liste isolée.
|
||||||
|
"""
|
||||||
|
if not expected_names:
|
||||||
|
return InstallResult(success=True)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["pacman", "-Su", "--noconfirm", "--noprogressbar"],
|
||||||
|
capture_output=True, text=True, timeout=600
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return InstallResult(success=True, installed=expected_names)
|
||||||
|
else:
|
||||||
|
return InstallResult(
|
||||||
|
success=False,
|
||||||
|
failed=expected_names,
|
||||||
|
error=result.stderr[-800:],
|
||||||
|
)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import state as state_mod
|
import state as state_mod
|
||||||
from package_manager import install_packages, is_pacman_locked
|
from package_manager import upgrade_cached_packages, is_pacman_locked
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@ -209,7 +209,7 @@ def run():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
display.update(f"Installation de {len(names)} paquet(s)…")
|
display.update(f"Installation de {len(names)} paquet(s)…")
|
||||||
result = install_packages(names)
|
result = upgrade_cached_packages(names)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Erreur inattendue : {e}")
|
logger.exception(f"Erreur inattendue : {e}")
|
||||||
|
|||||||
36
lib/ui.py
36
lib/ui.py
@ -59,6 +59,13 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
self.pending_group.set_description(
|
self.pending_group.set_description(
|
||||||
"Ces paquets seront installés automatiquement à la prochaine extinction du PC"
|
"Ces paquets seront installés automatiquement à la prochaine extinction du PC"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.install_now_btn = Gtk.Button.new_with_label("Installer maintenant")
|
||||||
|
self.install_now_btn.add_css_class("suggested-action")
|
||||||
|
self.install_now_btn.add_css_class("pill")
|
||||||
|
self.install_now_btn.connect("clicked", self._on_install_pending_clicked)
|
||||||
|
self.pending_group.set_header_suffix(self.install_now_btn)
|
||||||
|
|
||||||
main_box.append(self.pending_group)
|
main_box.append(self.pending_group)
|
||||||
|
|
||||||
# Groupe "Récemment installées"
|
# Groupe "Récemment installées"
|
||||||
@ -233,6 +240,10 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
pass
|
pass
|
||||||
self.status_sub_lbl.set_text(sub)
|
self.status_sub_lbl.set_text(sub)
|
||||||
|
|
||||||
|
# ── Bouton "Installer maintenant" ────────────────────────────────
|
||||||
|
can_install = status in ("idle", "error") and bool(pending)
|
||||||
|
self.install_now_btn.set_sensitive(can_install)
|
||||||
|
|
||||||
# ── 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)
|
||||||
@ -306,6 +317,31 @@ class UpdaterWindow(Adw.ApplicationWindow):
|
|||||||
self.refresh_btn.set_sensitive(False)
|
self.refresh_btn.set_sensitive(False)
|
||||||
threading.Thread(target=self._send_check_now, daemon=True).start()
|
threading.Thread(target=self._send_check_now, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_install_pending_clicked(self, _btn):
|
||||||
|
self.install_now_btn.set_sensitive(False)
|
||||||
|
threading.Thread(target=self._send_install_pending, daemon=True).start()
|
||||||
|
|
||||||
|
def _send_install_pending(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_pending"}).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_now_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_now_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