diff --git a/README.md b/README.md index e13e171..954ada3 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,15 @@ 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: +Il servizio Windows non controlla il server di backup: resta dedicato al solo watchdog dei file. La raggiungibilita' dello slave viene controllata dal notifier schedulato. + +Quando il notifier rileva che il server di backup non e' raggiungibile, mostra una finestra con: ```text 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: +Il notifier puo' essere eseguito da Task Scheduler con: ```powershell python -m bakrest.server_notifier @@ -66,7 +68,7 @@ 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. +Schedulazione consigliata: all'accesso dell'utente e poi ogni 30 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 @@ -115,9 +117,29 @@ robocopy_path = "robocopy" [backup.server_check] type = "tcp" port = 445 -interval_seconds = 600 +interval_seconds = 1800 ``` +## Task Scheduler + +Sono disponibili XML importabili in Task Scheduler: + +```text +tasks\BakRestBackupOnLogoff.xml +tasks\BakRestServerNotifierEvery30Minutes.xml +``` + +Importazione da PowerShell: + +```powershell +schtasks /Create /TN "BakRest\BackupOnLogoff" /XML "tasks\BakRestBackupOnLogoff.xml" /F +schtasks /Create /TN "BakRest\ServerNotifierEvery30Minutes" /XML "tasks\BakRestServerNotifierEvery30Minutes.xml" /F +``` + +Il task di backup usa l'evento Security `4647`, cioe' logoff/disconnessione iniziata dall'utente. Se sulla macchina questo evento non viene scritto, bisogna abilitare l'audit logoff oppure useremo un trigger alternativo. + +Il task notifier usa un trigger al logon e un trigger giornaliero con ripetizione `PT30M`, quindi avvisa durante la giornata anche se non viene usata la tray app. + 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: ```text diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 6843c8f..7205a0f 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -34,10 +34,9 @@ Il servizio Windows deve: - applicare esclusioni di directory e pattern; - registrare i file cambiati in un registro persistente; - mantenere un log giornaliero delle operazioni; -- verificare periodicamente la raggiungibilita' del server di backup; -- segnalare lo stato del server alla tray app. +- mantenere un log giornaliero delle operazioni. -Il controllo di raggiungibilita' del server deve avvenire ogni 10 minuti, salvo diversa configurazione. +Il servizio watchdog non deve controllare la raggiungibilita' del server di backup. Questo controllo deve essere gestito da Task Scheduler tramite notifier separato, in modo da poter mostrare avvisi nella sessione utente senza accoppiare il servizio a interfacce grafiche. La libreria Python consigliata per il monitoraggio e' `watchdog`, perche' su Windows usa API native di sistema ed evita un polling continuo e inefficiente. @@ -185,7 +184,7 @@ 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. +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 30 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 diff --git a/config/config.example.toml b/config/config.example.toml index 8cc7ebe..48de243 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 = 600 +interval_seconds = 1800 timeout_seconds = 3 [stability] diff --git a/src/bakrest/config.py b/src/bakrest/config.py index db079e1..9d8d85a 100644 --- a/src/bakrest/config.py +++ b/src/bakrest/config.py @@ -124,8 +124,8 @@ 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 + if isinstance(server_check, dict) and int(server_check.get("interval_seconds", 300)) in {300, 600}: + server_check["interval_seconds"] = 1800 changed = True destinations = backup.get("remote_destinations") diff --git a/src/bakrest/default_config.py b/src/bakrest/default_config.py index f82b84a..da5cd71 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 = 600 +interval_seconds = 1800 timeout_seconds = 3 [stability] diff --git a/src/bakrest/watcher.py b/src/bakrest/watcher.py index a4aebf8..e235a86 100644 --- a/src/bakrest/watcher.py +++ b/src/bakrest/watcher.py @@ -2,14 +2,12 @@ from __future__ import annotations import logging import threading -import time from pathlib import Path from .config import AppConfig from .filters import WatchFilter from .logging_setup import close_logger from .registry import ChangeEvent, ChangeRegistry -from .server_check import check_server, write_status class WatchdogRuntime: @@ -49,7 +47,7 @@ class WatchdogRuntime: observer.start() self.logger.info("BakRestWatchdog started") try: - self._run_health_loop() + self._run_service_loop() finally: observer.stop() observer.join(timeout=10) @@ -82,24 +80,9 @@ class WatchdogRuntime: self.registry.record(change) self.logger.info("Recorded %s: %s", event_type, target_path) - 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): - 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 _run_service_loop(self) -> None: + while not self.stop_event.wait(60): + pass def _safe_size(path: Path) -> int | None: diff --git a/tasks/BakRestBackupOnLogoff.xml b/tasks/BakRestBackupOnLogoff.xml new file mode 100644 index 0000000..b885288 --- /dev/null +++ b/tasks/BakRestBackupOnLogoff.xml @@ -0,0 +1,45 @@ + + + + BakRest + Run Bak&Rest no-GUI backup when the user logs off, then shut down on success. + + + + true + <QueryList><Query Id="0" Path="Security"><Select Path="Security">*[System[(EventID=4647)]]</Select></Query></QueryList> + + + + + S-1-5-32-545 + LeastPrivilege + + + + IgnoreNew + false + false + false + true + true + + false + false + + true + true + false + false + false + PT4H + 7 + + + + C:\Python314\python.exe + -m bakrest.tray_app --nogui + C:\devel\bak&rest + + + diff --git a/tasks/BakRestServerNotifierEvery30Minutes.xml b/tasks/BakRestServerNotifierEvery30Minutes.xml new file mode 100644 index 0000000..2c2acab --- /dev/null +++ b/tasks/BakRestServerNotifierEvery30Minutes.xml @@ -0,0 +1,56 @@ + + + + BakRest + Check the Bak&Rest backup server every 30 minutes and notify the user if it is unreachable. + + + + true + + + 2026-07-01T08:00:00 + true + + 1 + + + PT30M + P1D + false + + + + + + S-1-5-32-545 + LeastPrivilege + + + + IgnoreNew + false + false + true + true + false + + false + false + + true + true + false + false + false + PT5M + 7 + + + + C:\Python314\python.exe + -m bakrest.server_notifier + C:\devel\bak&rest + + + diff --git a/tests/test_config_and_filters.py b/tests/test_config_and_filters.py index c87499e..47cfbd7 100644 --- a/tests/test_config_and_filters.py +++ b/tests/test_config_and_filters.py @@ -66,7 +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"]["server_check"]["interval_seconds"] == 1800 assert data["backup"]["remote_destinations"] == [] diff --git a/tests/test_task_xml.py b/tests/test_task_xml.py new file mode 100644 index 0000000..4d9b494 --- /dev/null +++ b/tests/test_task_xml.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import xml.etree.ElementTree as ET +from pathlib import Path + + +def test_task_xml_files_are_parseable() -> None: + task_dir = Path("tasks") + files = [ + task_dir / "BakRestBackupOnLogoff.xml", + task_dir / "BakRestServerNotifierEvery30Minutes.xml", + ] + + for file in files: + ET.parse(file) + + +def test_notifier_task_runs_every_30_minutes() -> None: + xml = (Path("tasks") / "BakRestServerNotifierEvery30Minutes.xml").read_text( + encoding="utf-8" + ) + + assert "PT30M" in xml + assert "-m bakrest.server_notifier" in xml + + +def test_backup_task_uses_logoff_event_and_nogui_backup() -> None: + xml = (Path("tasks") / "BakRestBackupOnLogoff.xml").read_text(encoding="utf-8") + + assert "EventID=4647" in xml + assert "-m bakrest.tray_app --nogui" in xml