"""Non-blocking controls for the system Syncthing instance.""" import queue import subprocess import threading from tkinter import ttk, messagebox SERVICE = "syncthing.service" SYSTEMCTL = ("systemctl", "--user", "--no-ask-password") def service_status(): result = subprocess.run( [*SYSTEMCTL, "show", SERVICE, "--property=ActiveState", "--value"], capture_output=True, text=True, timeout=10, ) if result.returncode: raise RuntimeError(result.stderr.strip() or "Statut indisponible") return result.stdout.strip() or "unknown" def control_service(action): if action == "ensure" and service_status() == "active": return "active" command = "start" if action == "ensure" else action result = subprocess.run( [*SYSTEMCTL, command, SERVICE], capture_output=True, text=True, timeout=60, ) if result.returncode: raise RuntimeError(result.stderr.strip() or f"Échec de systemctl {command}") status = service_status() if command == "start" and status != "active": raise RuntimeError(f"Syncthing n’est pas actif : {status}") return status class SyncthingPanel(ttk.Frame): def __init__(self, parent): super().__init__(parent) self.events = queue.Queue() self.busy = False self.pending_export = None self.status = ttk.Label(self, text=f"{SERVICE} : vérification…") self.status.pack(anchor="w") controls = ttk.Frame(self) controls.pack(anchor="w", pady=4) self.start_button = ttk.Button(controls, text="Démarrer Syncthing", command=lambda: self.request("start")) self.start_button.pack(side="left") self.stop_button = ttk.Button(controls, text="Arrêter Syncthing", command=lambda: self.request("stop")) self.stop_button.pack(side="left", padx=6) self.timer = self.after(100, self.poll) self.refresh_timer = None self.bind("", self.cleanup, add=True) self.request("status") def ensure_started(self, callback): self.pending_export = callback if not self.busy: self.request("ensure") def request(self, action): if self.busy: return self.busy = True self.start_button.configure(state="disabled") self.stop_button.configure(state="disabled") if action != "status": self.status.configure(text=f"{SERVICE} : opération en cours…") def work(): try: status = service_status() if action == "status" else control_service(action) self.events.put((action, status, None)) except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc: self.events.put((action, None, str(exc))) threading.Thread(target=work, daemon=True).start() def poll(self): try: action, status, error = self.events.get_nowait() except queue.Empty: pass else: self.busy = False self.start_button.configure(state="normal") self.stop_button.configure(state="normal") self.status.configure(text=f"{SERVICE} : {error or status}") if action == "ensure": callback, self.pending_export = self.pending_export, None if error: messagebox.showwarning( "Syncthing", f"Impossible de démarrer Syncthing : {error}\n\n" "L’export va continuer.", ) if callback: callback() elif error and action != "status": messagebox.showerror("Syncthing", error) elif self.pending_export: self.request("ensure") if self.winfo_exists(): self.timer = self.after(100, self.poll) if self.refresh_timer is None: self.refresh_timer = self.after(5000, self.refresh) def refresh(self): self.refresh_timer = None self.request("status") def cleanup(self, event): if event.widget == self: self.pending_export = None for timer in (self.timer, self.refresh_timer): if timer: self.after_cancel(timer)