From ad7265183cc60c2b0639e8f1c5c86520724d523e Mon Sep 17 00:00:00 2001 From: allebonvi Date: Wed, 1 Jul 2026 12:51:32 +0200 Subject: [PATCH] Avvisa se server backup non raggiungibile --- README.md | 7 ++++++ SPECIFICHE.md | 8 +++++++ config/config.example.toml | 2 +- src/bakrest/config.py | 3 +++ src/bakrest/default_config.py | 2 +- src/bakrest/tray_app.py | 38 ++++++++++++++++++++++++++++---- src/bakrest/watcher.py | 26 +++++++++++++--------- tests/test_config_and_filters.py | 1 + 8 files changed, 70 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7612365..ed8254d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,12 @@ python -m bakrest.tray_app La tray app legge lo stato prodotto dal servizio e offre il comando `Backup e spegni`. Se il backup fallisce, lo spegnimento non viene eseguito. +Quando il servizio rileva che il server di backup non e' raggiungibile, la tray app mostra una finestra con: + +```text +Accendi il server di backup o verifica che sia connesso alla rete +``` + ## Backup senza GUI per Task Scheduler Lo stesso entry point della tray puo' eseguire il backup senza mostrare icone o finestre: @@ -93,6 +99,7 @@ robocopy_path = "robocopy" [backup.server_check] type = "tcp" port = 445 +interval_seconds = 600 ``` La copia ricrea il percorso relativo sotto ogni share. Se una cartella monitorata e' `D:\Lavori`, il file `D:\Lavori\Cliente\a.psd` viene copiato in: diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 091d79d..0eca5cf 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -37,6 +37,8 @@ Il servizio Windows deve: - verificare periodicamente la raggiungibilita' del server di backup; - segnalare lo stato del server alla tray app. +Il controllo di raggiungibilita' del server deve avvenire ogni 10 minuti, salvo diversa configurazione. + La libreria Python consigliata per il monitoraggio e' `watchdog`, perche' su Windows usa API native di sistema ed evita un polling continuo e inefficiente. ### Tray app @@ -177,6 +179,12 @@ La tray app deve mostrare chiaramente se il server risulta spento o non raggiung La non raggiungibilita' del server deve essere registrata nei log giornalieri. +Poiche' un servizio Windows non deve aprire direttamente finestre nella sessione utente, la finestra di avviso deve essere mostrata dalla tray app quando legge uno stato server non raggiungibile. Il messaggio richiesto e': + +```text +Accendi il server di backup o verifica che sia connesso alla rete +``` + ## Strategia di monitoraggio Non e' consigliato monitorare tutto il disco filtrando solo per estensione, ad esempio `C:\` con filtro `.jpg`, `.png`, `.mp4`. Questo approccio puo' generare molti eventi inutili, includere directory di sistema, rallentare il sistema e creare un registro troppo rumoroso. diff --git a/config/config.example.toml b/config/config.example.toml index 8e32408..f8f4b36 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -57,7 +57,7 @@ shutdown_command = "shutdown /s /t 0" [backup.server_check] type = "tcp" port = 445 -interval_seconds = 300 +interval_seconds = 600 timeout_seconds = 3 [stability] diff --git a/src/bakrest/config.py b/src/bakrest/config.py index 4cb78ea..1008b22 100644 --- a/src/bakrest/config.py +++ b/src/bakrest/config.py @@ -123,6 +123,9 @@ def migrate_config_data(data: dict[str, Any]) -> bool: if isinstance(server_check, dict) and int(server_check.get("port", 22)) == 22: server_check["port"] = 445 changed = True + if isinstance(server_check, dict) and int(server_check.get("interval_seconds", 300)) == 300: + server_check["interval_seconds"] = 600 + changed = True destinations = backup.get("remote_destinations") if not isinstance(destinations, list): diff --git a/src/bakrest/default_config.py b/src/bakrest/default_config.py index d184904..945971d 100644 --- a/src/bakrest/default_config.py +++ b/src/bakrest/default_config.py @@ -57,7 +57,7 @@ shutdown_command = "shutdown /s /t 0" [backup.server_check] type = "tcp" port = 445 -interval_seconds = 300 +interval_seconds = 600 timeout_seconds = 3 [stability] diff --git a/src/bakrest/tray_app.py b/src/bakrest/tray_app.py index d3a327f..f8a080a 100644 --- a/src/bakrest/tray_app.py +++ b/src/bakrest/tray_app.py @@ -4,6 +4,7 @@ import argparse import json import logging import threading +import time import tkinter as tk from tkinter import messagebox from pathlib import Path @@ -23,6 +24,8 @@ class BakRestTrayApp: self.config = config self.logger = logger self.registry = ChangeRegistry(config.storage.change_registry) + self.stop_event = threading.Event() + self.last_alert_reachable: bool | None = None self.icon = pystray.Icon( "BakRestTray", _create_icon_image(), @@ -32,6 +35,7 @@ class BakRestTrayApp: def run(self) -> None: self.logger.info("BakRestTray started") + threading.Thread(target=self._server_status_monitor, daemon=True).start() self.icon.run() def _build_menu(self) -> pystray.Menu: @@ -49,14 +53,19 @@ class BakRestTrayApp: return f"Server: {server} | File in attesa: {pending}" def _read_server_status(self) -> str: + status = self._read_server_status_data() + if status is None: + return "sconosciuto" + return "raggiungibile" if status.get("reachable") else "non raggiungibile" + + def _read_server_status_data(self) -> dict[str, object] | None: path = self.config.storage.status_file if not path.exists(): - return "sconosciuto" + return None try: - data = json.loads(path.read_text(encoding="utf-8")) + return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): - return "errore stato" - return "raggiungibile" if data.get("reachable") else "non raggiungibile" + return None def _backup_and_shutdown(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: threading.Thread(target=self._backup_and_shutdown_worker, daemon=True).start() @@ -81,8 +90,29 @@ class BakRestTrayApp: def _quit(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: self.logger.info("BakRestTray stopped") + self.stop_event.set() self.icon.stop() + def _server_status_monitor(self) -> None: + interval = min(max(self.config.backup.server_check.interval_seconds, 60), 600) + while not self.stop_event.wait(interval): + status = self._read_server_status_data() + if status is None: + continue + + reachable = bool(status.get("reachable")) + if reachable: + self.last_alert_reachable = True + continue + + if self.last_alert_reachable is not False: + self.last_alert_reachable = False + self.logger.warning("Showing backup server unreachable alert") + _show_error_message( + "Bak&Rest", + "Accendi il server di backup o verifica che sia connesso alla rete", + ) + def _create_icon_image() -> Image.Image: image = Image.new("RGBA", (64, 64), (26, 38, 52, 255)) diff --git a/src/bakrest/watcher.py b/src/bakrest/watcher.py index 7c3672c..a4aebf8 100644 --- a/src/bakrest/watcher.py +++ b/src/bakrest/watcher.py @@ -84,18 +84,22 @@ class WatchdogRuntime: def _run_health_loop(self) -> None: interval = self.config.backup.server_check.interval_seconds + self._publish_server_status() while not self.stop_event.wait(interval): - status = check_server(self.config.backup) - pending_count = self.registry.pending_count() - write_status(self.config.storage.status_file, status, pending_count) - if status.reachable: - self.logger.info("Backup server reachable; pending files: %s", pending_count) - else: - self.logger.warning( - "Backup server unreachable: %s; pending files: %s", - status.message, - pending_count, - ) + self._publish_server_status() + + def _publish_server_status(self) -> None: + status = check_server(self.config.backup) + pending_count = self.registry.pending_count() + write_status(self.config.storage.status_file, status, pending_count) + if status.reachable: + self.logger.info("Backup server reachable; pending files: %s", pending_count) + else: + self.logger.warning( + "Backup server unreachable: %s; pending files: %s", + status.message, + pending_count, + ) def _safe_size(path: Path) -> int | None: diff --git a/tests/test_config_and_filters.py b/tests/test_config_and_filters.py index e92e69e..c87499e 100644 --- a/tests/test_config_and_filters.py +++ b/tests/test_config_and_filters.py @@ -66,6 +66,7 @@ def test_migrate_legacy_rsync_config_to_robocopy_defaults() -> None: assert data["backup"]["engine"] == "robocopy" assert data["backup"]["robocopy_path"] == "robocopy" assert data["backup"]["server_check"]["port"] == 445 + assert data["backup"]["server_check"]["interval_seconds"] == 600 assert data["backup"]["remote_destinations"] == []