Aggiunge notifier server per scheduler
This commit is contained in:
16
README.md
16
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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
"""
|
||||
|
||||
83
src/bakrest/server_notifier.py
Normal file
83
src/bakrest/server_notifier.py
Normal 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())
|
||||
18
tests/test_server_notifier.py
Normal file
18
tests/test_server_notifier.py
Normal 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)
|
||||
Reference in New Issue
Block a user