Aggiunge notifier server per scheduler

This commit is contained in:
2026-07-01 12:56:27 +02:00
parent ad7265183c
commit 6232bb240c
7 changed files with 137 additions and 2 deletions

View File

@@ -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 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 ## Backup senza GUI per Task Scheduler
Lo stesso entry point della tray puo' eseguire il backup senza mostrare icone o finestre: Lo stesso entry point della tray puo' eseguire il backup senza mostrare icone o finestre:

View File

@@ -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 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 ## 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. 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.

View File

@@ -69,3 +69,4 @@ unchanged_checks_required = 2
change_registry = "%PROGRAMDATA%\\BakRest\\changes.sqlite" change_registry = "%PROGRAMDATA%\\BakRest\\changes.sqlite"
logs_dir = "%PROGRAMDATA%\\BakRest\\logs" logs_dir = "%PROGRAMDATA%\\BakRest\\logs"
status_file = "%PROGRAMDATA%\\BakRest\\status.json" status_file = "%PROGRAMDATA%\\BakRest\\status.json"
notifier_state_file = "%PROGRAMDATA%\\BakRest\\notifier-state.json"

View File

@@ -46,6 +46,7 @@ class StorageConfig:
change_registry: Path change_registry: Path
logs_dir: Path logs_dir: Path
status_file: Path status_file: Path
notifier_state_file: Path
@dataclass(frozen=True) @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: def migrate_config_data(data: dict[str, Any]) -> bool:
changed = _migrate_storage_config(data)
backup = data.get("backup") backup = data.get("backup")
if not isinstance(backup, dict): if not isinstance(backup, dict):
return False return changed
changed = False
engine = str(backup.get("engine", "robocopy")).lower() engine = str(backup.get("engine", "robocopy")).lower()
if "engine" not in backup: if "engine" not in backup:
backup["engine"] = engine backup["engine"] = engine
@@ -138,6 +139,16 @@ def migrate_config_data(data: dict[str, Any]) -> bool:
return changed 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: def parse_config(data: dict[str, Any], path: Path) -> AppConfig:
service = data.get("service", {}) service = data.get("service", {})
tray = data.get("tray", {}) 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")), logs_dir=expand_path(storage.get("logs_dir", "%PROGRAMDATA%\\BakRest\\logs")),
status_file=expand_path(storage.get("status_file", "%PROGRAMDATA%\\BakRest\\status.json")), 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( return AppConfig(

View File

@@ -69,4 +69,5 @@ unchanged_checks_required = 2
change_registry = "%PROGRAMDATA%\\\\BakRest\\\\changes.sqlite" change_registry = "%PROGRAMDATA%\\\\BakRest\\\\changes.sqlite"
logs_dir = "%PROGRAMDATA%\\\\BakRest\\\\logs" logs_dir = "%PROGRAMDATA%\\\\BakRest\\\\logs"
status_file = "%PROGRAMDATA%\\\\BakRest\\\\status.json" status_file = "%PROGRAMDATA%\\\\BakRest\\\\status.json"
notifier_state_file = "%PROGRAMDATA%\\\\BakRest\\\\notifier-state.json"
""" """

View File

@@ -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())

View File

@@ -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)