From 6232bb240c863f23b871c4ea35fe4fb6faf47541 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Wed, 1 Jul 2026 12:56:27 +0200 Subject: [PATCH] Aggiunge notifier server per scheduler --- README.md | 16 +++++++ SPECIFICHE.md | 2 + config/config.example.toml | 1 + src/bakrest/config.py | 18 +++++++- src/bakrest/default_config.py | 1 + src/bakrest/server_notifier.py | 83 ++++++++++++++++++++++++++++++++++ tests/test_server_notifier.py | 18 ++++++++ 7 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/bakrest/server_notifier.py create mode 100644 tests/test_server_notifier.py diff --git a/README.md b/README.md index ed8254d..e13e171 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,22 @@ Quando il servizio rileva che il server di backup non e' raggiungibile, la tray Accendi il server di backup o verifica che sia connesso alla rete ``` +Se non si vuole usare la tray durante la giornata, lo stesso avviso puo' essere gestito da Task Scheduler con: + +```powershell +python -m bakrest.server_notifier +``` + +Azione Task Scheduler consigliata: + +```text +Programma: C:\Python314\python.exe +Argomenti: -m bakrest.server_notifier +Avvia in: C:\devel\bak&rest +``` + +Schedulazione consigliata: all'accesso dell'utente e poi ogni 10 minuti. Il notifier mostra l'avviso una sola volta mentre il server resta non raggiungibile, poi si resetta quando il server torna raggiungibile. + ## Backup senza GUI per Task Scheduler Lo stesso entry point della tray puo' eseguire il backup senza mostrare icone o finestre: diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 0eca5cf..6843c8f 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -185,6 +185,8 @@ Poiche' un servizio Windows non deve aprire direttamente finestre nella sessione Accendi il server di backup o verifica che sia connesso alla rete ``` +Se il flusso operativo usa solo Task Scheduler e backup no-GUI, l'avviso durante la giornata deve essere gestito da un notifier separato lanciato nella sessione utente, sempre tramite Task Scheduler. Il notifier deve controllare il server ogni 10 minuti, mostrare il messaggio una sola volta mentre il server resta non raggiungibile, e resettare l'avviso quando il server torna raggiungibile. + ## 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 f8f4b36..8cc7ebe 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -69,3 +69,4 @@ unchanged_checks_required = 2 change_registry = "%PROGRAMDATA%\\BakRest\\changes.sqlite" logs_dir = "%PROGRAMDATA%\\BakRest\\logs" status_file = "%PROGRAMDATA%\\BakRest\\status.json" +notifier_state_file = "%PROGRAMDATA%\\BakRest\\notifier-state.json" diff --git a/src/bakrest/config.py b/src/bakrest/config.py index 1008b22..db079e1 100644 --- a/src/bakrest/config.py +++ b/src/bakrest/config.py @@ -46,6 +46,7 @@ class StorageConfig: change_registry: Path logs_dir: Path status_file: Path + notifier_state_file: Path @dataclass(frozen=True) @@ -104,11 +105,11 @@ def save_config_data(data: dict[str, Any], path: Path | None = None) -> Path: def migrate_config_data(data: dict[str, Any]) -> bool: + changed = _migrate_storage_config(data) backup = data.get("backup") if not isinstance(backup, dict): - return False + return changed - changed = False engine = str(backup.get("engine", "robocopy")).lower() if "engine" not in backup: backup["engine"] = engine @@ -138,6 +139,16 @@ def migrate_config_data(data: dict[str, Any]) -> bool: return changed +def _migrate_storage_config(data: dict[str, Any]) -> bool: + storage = data.get("storage") + if not isinstance(storage, dict): + return False + if "notifier_state_file" in storage: + return False + storage["notifier_state_file"] = "%PROGRAMDATA%\\BakRest\\notifier-state.json" + return True + + def parse_config(data: dict[str, Any], path: Path) -> AppConfig: service = data.get("service", {}) tray = data.get("tray", {}) @@ -159,6 +170,9 @@ def parse_config(data: dict[str, Any], path: Path) -> AppConfig: ), logs_dir=expand_path(storage.get("logs_dir", "%PROGRAMDATA%\\BakRest\\logs")), status_file=expand_path(storage.get("status_file", "%PROGRAMDATA%\\BakRest\\status.json")), + notifier_state_file=expand_path( + storage.get("notifier_state_file", "%PROGRAMDATA%\\BakRest\\notifier-state.json") + ), ) return AppConfig( diff --git a/src/bakrest/default_config.py b/src/bakrest/default_config.py index 945971d..f82b84a 100644 --- a/src/bakrest/default_config.py +++ b/src/bakrest/default_config.py @@ -69,4 +69,5 @@ unchanged_checks_required = 2 change_registry = "%PROGRAMDATA%\\\\BakRest\\\\changes.sqlite" logs_dir = "%PROGRAMDATA%\\\\BakRest\\\\logs" status_file = "%PROGRAMDATA%\\\\BakRest\\\\status.json" +notifier_state_file = "%PROGRAMDATA%\\\\BakRest\\\\notifier-state.json" """ diff --git a/src/bakrest/server_notifier.py b/src/bakrest/server_notifier.py new file mode 100644 index 0000000..a1ffea3 --- /dev/null +++ b/src/bakrest/server_notifier.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import argparse +import json +import tkinter as tk +from pathlib import Path +from tkinter import messagebox + +from .config import load_config +from .logging_setup import close_logger, configure_logging +from .registry import ChangeRegistry +from .server_check import ServerStatus, check_server, write_status + + +ALERT_MESSAGE = "Accendi il server di backup o verifica che sia connesso alla rete" + + +def run_server_notifier(always_alert: bool = False) -> int: + config = load_config() + logger = configure_logging(config.storage.logs_dir, "server-notifier") + try: + status = check_server(config.backup) + pending_count = ChangeRegistry(config.storage.change_registry).pending_count() + write_status(config.storage.status_file, status, pending_count) + + if status.reachable: + logger.info("Backup server reachable") + _write_state(config.storage.notifier_state_file, "reachable") + return 0 + + logger.warning("Backup server unreachable: %s", status.message) + previous_state = _read_state(config.storage.notifier_state_file) + if always_alert or previous_state != "unreachable": + _show_error_message("Bak&Rest", ALERT_MESSAGE) + _write_state(config.storage.notifier_state_file, "unreachable") + return 20 + finally: + close_logger(logger) + + +def should_alert(status: ServerStatus, previous_state: str | None, always_alert: bool = False) -> bool: + if status.reachable: + return False + return always_alert or previous_state != "unreachable" + + +def _read_state(path: Path) -> str | None: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + state = data.get("server_state") + return str(state) if state else None + + +def _write_state(path: Path, state: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({"server_state": state}, indent=2), encoding="utf-8") + + +def _show_error_message(title: str, message: str) -> None: + root = tk.Tk() + root.withdraw() + root.attributes("-topmost", True) + try: + messagebox.showerror(title, message, parent=root) + finally: + root.destroy() + + +def main() -> int: + parser = argparse.ArgumentParser(description="Notify the user if the backup server is unreachable") + parser.add_argument( + "--always-alert", + action="store_true", + help="Show the warning even if it was already shown while the server was down", + ) + args = parser.parse_args() + return run_server_notifier(always_alert=args.always_alert) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_server_notifier.py b/tests/test_server_notifier.py new file mode 100644 index 0000000..432a734 --- /dev/null +++ b/tests/test_server_notifier.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from bakrest.server_check import ServerStatus +from bakrest.server_notifier import should_alert + + +def test_notifier_alerts_once_while_server_is_down() -> None: + status = ServerStatus(False, "now", "down") + + assert should_alert(status, None) + assert not should_alert(status, "unreachable") + assert should_alert(status, "unreachable", always_alert=True) + + +def test_notifier_does_not_alert_when_server_is_reachable() -> None: + status = ServerStatus(True, "now", "ok") + + assert not should_alert(status, None)