e35b352a2a
Implement a graphical interface for the FWS system recovery tool using GTK3. Features: - Scans for FWS installations on disk - Provides action buttons for common repairs: GRUB reinstall, initramfs regeneration, pacman repair, and full system repair - Displays real-time engine output in a log viewer - Supports both root session (live mode) and regular user (pkexec elevation) - Includes terminal access (chroot) for manual diagnostics - Offers system reboot/poweroff controls The GUI replaces Arch Linux branding with FWS in output for consistency while preserving technical identifiers (package names, file paths) for accurate diagnostics.
270 lines
11 KiB
Python
Executable File
270 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# FWS — application GRAPHIQUE de réparation du système (mode « Réparer FWS »
|
|
# du live). GTK3 : le live embarque python-gobject/gtk3 via Anaconda.
|
|
#
|
|
# Pilote le MOTEUR fws-recovery en sous-commandes :
|
|
# fws-recovery scan → détecte les installations FWS
|
|
# fws-recovery repair <action> <dev> → répare (sortie streamée ici)
|
|
# fws-recovery shell <dev> → terminal chroot (xterm)
|
|
#
|
|
# Deux contextes de lancement :
|
|
# • mode « Réparer FWS » (live OU entrée GRUB de l'OS installé) : session
|
|
# root via fws-recovery-session → le moteur est appelé directement ;
|
|
# • menu d'applications de l'OS installé : lancée en UTILISATEUR → chaque
|
|
# appel du moteur passe par pkexec (politique org.fws.recovery,
|
|
# auth_admin_keep : un seul mot de passe pour toute la session).
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import GLib, Gtk, Pango # noqa: E402
|
|
|
|
GLib.set_prgname("fws-recovery-gui")
|
|
|
|
_ENGINE = "/usr/local/bin/fws-recovery"
|
|
|
|
|
|
def _engine_cmd(args):
|
|
"""Commande moteur, élevée via pkexec si on n'est pas root."""
|
|
cmd = [_ENGINE] + args
|
|
if os.geteuid() != 0:
|
|
cmd = ["pkexec"] + cmd
|
|
return cmd
|
|
|
|
_ACTIONS = [
|
|
("grub", "Réinstaller GRUB",
|
|
"le système ne démarre plus / « Operating System not found »"),
|
|
("initramfs", "Régénérer l'initramfs",
|
|
"kernel panic au démarrage, module manquant (mkinitcpio -P)"),
|
|
("pacman", "Réparer pacman",
|
|
"verrou bloqué, clés de signature cassées"),
|
|
("pacman-update", "Réparer pacman + mise à jour complète",
|
|
"idem, puis pacman -Syu (réseau requis)"),
|
|
("all", "Tout réparer",
|
|
"initramfs puis GRUB — le réflexe « ça ne boote plus »"),
|
|
]
|
|
|
|
|
|
class RecoveryWindow(Gtk.Window):
|
|
def __init__(self):
|
|
super().__init__(title="FWS — Réparation du système")
|
|
self.set_icon_name("fws-recovery")
|
|
self.set_default_size(720, 560)
|
|
self.set_position(Gtk.WindowPosition.CENTER)
|
|
self.set_border_width(18)
|
|
self.connect("destroy", Gtk.main_quit)
|
|
|
|
self._proc = None
|
|
self._installs = [] # [(dev, nom), …]
|
|
self._action_buttons = []
|
|
|
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
|
self.add(root)
|
|
|
|
title = Gtk.Label()
|
|
title.set_markup("<span size='x-large' weight='bold'>Réparation du système FWS</span>")
|
|
root.pack_start(title, False, False, 0)
|
|
|
|
self._status = Gtk.Label(label="Recherche des installations FWS sur les disques…")
|
|
self._status.set_xalign(0)
|
|
root.pack_start(self._status, False, False, 0)
|
|
|
|
# Sélecteur d'installation (rempli après le scan).
|
|
picker_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
picker_row.pack_start(Gtk.Label(label="Système à réparer :"), False, False, 0)
|
|
self._picker = Gtk.ComboBoxText()
|
|
picker_row.pack_start(self._picker, True, True, 0)
|
|
root.pack_start(picker_row, False, False, 0)
|
|
|
|
# Boutons d'action (style fws-hello : titre + description).
|
|
for key, label, desc in _ACTIONS:
|
|
button = Gtk.Button()
|
|
inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
|
lab = Gtk.Label()
|
|
lab.set_markup(f"<b>{label}</b>")
|
|
lab.set_xalign(0)
|
|
sub = Gtk.Label(label=desc)
|
|
sub.set_xalign(0)
|
|
sub.get_style_context().add_class("dim-label")
|
|
inner.pack_start(lab, False, False, 0)
|
|
inner.pack_start(sub, False, False, 0)
|
|
button.add(inner)
|
|
button.connect("clicked", self._on_action, key)
|
|
root.pack_start(button, False, False, 0)
|
|
self._action_buttons.append(button)
|
|
|
|
shell_btn = Gtk.Button(label="Ouvrir un terminal dans le système (chroot)")
|
|
shell_btn.connect("clicked", self._on_shell)
|
|
root.pack_start(shell_btn, False, False, 0)
|
|
self._action_buttons.append(shell_btn)
|
|
|
|
# Journal (sortie du moteur, en continu).
|
|
frame = Gtk.Frame(label="Journal")
|
|
scroll = Gtk.ScrolledWindow()
|
|
scroll.set_min_content_height(150)
|
|
self._log = Gtk.TextView()
|
|
self._log.set_editable(False)
|
|
self._log.set_cursor_visible(False)
|
|
self._log.set_monospace(True)
|
|
self._log.modify_font(Pango.FontDescription("Monospace 9"))
|
|
scroll.add(self._log)
|
|
frame.add(scroll)
|
|
root.pack_start(frame, True, True, 0)
|
|
|
|
# Pied : redémarrer / éteindre.
|
|
footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
reboot = Gtk.Button(label="Redémarrer")
|
|
reboot.connect("clicked", lambda _b: self._power("reboot"))
|
|
poweroff = Gtk.Button(label="Éteindre")
|
|
poweroff.connect("clicked", lambda _b: self._power("poweroff"))
|
|
footer.pack_end(reboot, False, False, 0)
|
|
footer.pack_end(poweroff, False, False, 0)
|
|
root.pack_start(footer, False, False, 0)
|
|
|
|
self._set_busy(True)
|
|
GLib.idle_add(self._start_scan)
|
|
|
|
# ── Utilitaires ─────────────────────────────────────────────────────────
|
|
def _append(self, text):
|
|
# Rebranding d'affichage : les outils sous-jacents parlent
|
|
# d'« Arch Linux » — pas le journal FWS. (Les identifiants techniques
|
|
# comme archlinux-keyring restent tels quels : ce sont de vrais noms
|
|
# de fichiers/paquets, les masquer fausserait le diagnostic.)
|
|
text = text.replace("Arch Linux", "FWS").replace("Fedora", "FWS")
|
|
buf = self._log.get_buffer()
|
|
buf.insert(buf.get_end_iter(), text)
|
|
mark = buf.create_mark(None, buf.get_end_iter(), False)
|
|
self._log.scroll_mark_onscreen(mark)
|
|
buf.delete_mark(mark)
|
|
|
|
def _set_busy(self, busy):
|
|
for b in self._action_buttons:
|
|
b.set_sensitive(not busy and bool(self._installs))
|
|
# Sensible même avec UNE seule install : un combo grisé donne
|
|
# l'impression que TOUTE l'app est désactivée (retour utilisateur).
|
|
self._picker.set_sensitive(not busy and bool(self._installs))
|
|
|
|
def _selected_dev(self):
|
|
idx = self._picker.get_active()
|
|
if 0 <= idx < len(self._installs):
|
|
return self._installs[idx][0]
|
|
return None
|
|
|
|
def _power(self, what):
|
|
subprocess.run(["systemctl", what], check=False)
|
|
|
|
# ── Scan des installations ──────────────────────────────────────────────
|
|
def _start_scan(self):
|
|
self._run_engine(["scan"], self._scan_done, quiet=True)
|
|
return False
|
|
|
|
def _scan_done(self, rc, output):
|
|
self._installs = []
|
|
for line in output.splitlines():
|
|
if "|" in line:
|
|
dev, _, name = line.partition("|")
|
|
self._installs.append((dev.strip(), name.strip()))
|
|
self._picker.append_text(f"{name.strip()} — {dev.strip()}")
|
|
if self._installs:
|
|
self._picker.set_active(0)
|
|
n = len(self._installs)
|
|
self._status.set_markup(
|
|
f"<b>{n}</b> installation{'s' if n > 1 else ''} FWS détectée{'s' if n > 1 else ''}."
|
|
" Choisis une réparation :")
|
|
elif rc in (126, 127):
|
|
# pkexec : authentification annulée ou refusée.
|
|
self._status.set_markup(
|
|
"<b>Authentification annulée.</b> Ferme puis relance"
|
|
" l'application pour réessayer.")
|
|
else:
|
|
self._status.set_markup(
|
|
"<b>Aucune installation FWS détectée.</b> Vérifie les disques"
|
|
" (terminal ci-dessous) ou redémarre.")
|
|
self._append("Aucune installation FWS trouvée.\n\n"
|
|
+ subprocess.run(["lsblk", "-o", "NAME,SIZE,TYPE,FSTYPE"],
|
|
capture_output=True, text=True,
|
|
check=False).stdout)
|
|
self._set_busy(False)
|
|
# Le terminal reste utile même sans installation détectée (diagnostic).
|
|
self._action_buttons[-1].set_sensitive(True)
|
|
|
|
# ── Exécution du moteur avec sortie en continu ──────────────────────────
|
|
def _run_engine(self, args, done_cb, quiet=False):
|
|
self._set_busy(True)
|
|
self._proc = subprocess.Popen(
|
|
_engine_cmd(args), stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT, text=True, bufsize=1)
|
|
chunks = []
|
|
|
|
def on_io(source, _cond):
|
|
line = source.readline()
|
|
if line:
|
|
chunks.append(line)
|
|
if not quiet:
|
|
self._append(line)
|
|
return True
|
|
rc = self._proc.wait()
|
|
done_cb(rc, "".join(chunks))
|
|
return False
|
|
|
|
GLib.io_add_watch(self._proc.stdout,
|
|
GLib.IO_IN | GLib.IO_HUP, on_io)
|
|
|
|
def _on_action(self, _button, key):
|
|
dev = self._selected_dev()
|
|
if not dev:
|
|
return
|
|
label = next(lab for k, lab, _d in _ACTIONS if k == key)
|
|
self._status.set_markup(f"<b>{label}</b> sur {dev} — en cours…")
|
|
self._append(f"\n───── {label} ({dev}) ─────\n")
|
|
self._run_engine(["repair", key, dev], self._action_done)
|
|
|
|
def _action_done(self, rc, _output):
|
|
if rc == 0:
|
|
self._status.set_markup("<b>Terminé ✔</b> — autre réparation, ou redémarre.")
|
|
elif rc in (126, 127):
|
|
self._status.set_markup(
|
|
"<b>Authentification annulée</b> — rien n'a été modifié.")
|
|
else:
|
|
self._status.set_markup(
|
|
f"<b>Échec (code {rc})</b> — détail dans le journal ci-dessous.")
|
|
self._set_busy(False)
|
|
|
|
def _on_shell(self, _button):
|
|
dev = self._selected_dev()
|
|
if dev and os.geteuid() == 0:
|
|
subprocess.Popen([_ENGINE, "shell", dev])
|
|
elif dev:
|
|
# pkexec purge DISPLAY → le moteur ouvre le chroot DANS cet xterm
|
|
# (lancé en utilisateur, donc visible sur X11 comme Wayland).
|
|
subprocess.Popen(["xterm", "-title",
|
|
"FWS — terminal dans le système (chroot)",
|
|
"-e", "pkexec", _ENGINE, "shell", dev])
|
|
elif shutil.which("xterm"):
|
|
subprocess.Popen(["xterm", "-title", "FWS — terminal"])
|
|
|
|
|
|
def main():
|
|
if not Gtk.init_check()[0]:
|
|
# Pas d'affichage : le .zprofile retombera sur l'assistant console.
|
|
return 1
|
|
if os.geteuid() != 0 and not shutil.which("pkexec"):
|
|
dlg = Gtk.MessageDialog(message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.CLOSE,
|
|
text="pkexec introuvable : lance fws-recovery-gui en root.")
|
|
dlg.run()
|
|
return 1
|
|
win = RecoveryWindow()
|
|
win.show_all()
|
|
Gtk.main()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|