Avvisa se server backup non raggiungibile

This commit is contained in:
2026-07-01 12:51:32 +02:00
parent 3573b4c149
commit ad7265183c
8 changed files with 70 additions and 17 deletions

View File

@@ -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. 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 ## 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:
@@ -93,6 +99,7 @@ robocopy_path = "robocopy"
[backup.server_check] [backup.server_check]
type = "tcp" type = "tcp"
port = 445 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: 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:

View File

@@ -37,6 +37,8 @@ Il servizio Windows deve:
- verificare periodicamente la raggiungibilita' del server di backup; - verificare periodicamente la raggiungibilita' del server di backup;
- segnalare lo stato del server alla tray app. - 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. 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 ### 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. 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 ## 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

@@ -57,7 +57,7 @@ shutdown_command = "shutdown /s /t 0"
[backup.server_check] [backup.server_check]
type = "tcp" type = "tcp"
port = 445 port = 445
interval_seconds = 300 interval_seconds = 600
timeout_seconds = 3 timeout_seconds = 3
[stability] [stability]

View File

@@ -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: if isinstance(server_check, dict) and int(server_check.get("port", 22)) == 22:
server_check["port"] = 445 server_check["port"] = 445
changed = True 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") destinations = backup.get("remote_destinations")
if not isinstance(destinations, list): if not isinstance(destinations, list):

View File

@@ -57,7 +57,7 @@ shutdown_command = "shutdown /s /t 0"
[backup.server_check] [backup.server_check]
type = "tcp" type = "tcp"
port = 445 port = 445
interval_seconds = 300 interval_seconds = 600
timeout_seconds = 3 timeout_seconds = 3
[stability] [stability]

View File

@@ -4,6 +4,7 @@ import argparse
import json import json
import logging import logging
import threading import threading
import time
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
from pathlib import Path from pathlib import Path
@@ -23,6 +24,8 @@ class BakRestTrayApp:
self.config = config self.config = config
self.logger = logger self.logger = logger
self.registry = ChangeRegistry(config.storage.change_registry) self.registry = ChangeRegistry(config.storage.change_registry)
self.stop_event = threading.Event()
self.last_alert_reachable: bool | None = None
self.icon = pystray.Icon( self.icon = pystray.Icon(
"BakRestTray", "BakRestTray",
_create_icon_image(), _create_icon_image(),
@@ -32,6 +35,7 @@ class BakRestTrayApp:
def run(self) -> None: def run(self) -> None:
self.logger.info("BakRestTray started") self.logger.info("BakRestTray started")
threading.Thread(target=self._server_status_monitor, daemon=True).start()
self.icon.run() self.icon.run()
def _build_menu(self) -> pystray.Menu: def _build_menu(self) -> pystray.Menu:
@@ -49,14 +53,19 @@ class BakRestTrayApp:
return f"Server: {server} | File in attesa: {pending}" return f"Server: {server} | File in attesa: {pending}"
def _read_server_status(self) -> str: 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 path = self.config.storage.status_file
if not path.exists(): if not path.exists():
return "sconosciuto" return None
try: try:
data = json.loads(path.read_text(encoding="utf-8")) return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError):
return "errore stato" return None
return "raggiungibile" if data.get("reachable") else "non raggiungibile"
def _backup_and_shutdown(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: def _backup_and_shutdown(self, icon: pystray.Icon, item: pystray.MenuItem) -> None:
threading.Thread(target=self._backup_and_shutdown_worker, daemon=True).start() 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: def _quit(self, icon: pystray.Icon, item: pystray.MenuItem) -> None:
self.logger.info("BakRestTray stopped") self.logger.info("BakRestTray stopped")
self.stop_event.set()
self.icon.stop() 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: def _create_icon_image() -> Image.Image:
image = Image.new("RGBA", (64, 64), (26, 38, 52, 255)) image = Image.new("RGBA", (64, 64), (26, 38, 52, 255))

View File

@@ -84,18 +84,22 @@ class WatchdogRuntime:
def _run_health_loop(self) -> None: def _run_health_loop(self) -> None:
interval = self.config.backup.server_check.interval_seconds interval = self.config.backup.server_check.interval_seconds
self._publish_server_status()
while not self.stop_event.wait(interval): while not self.stop_event.wait(interval):
status = check_server(self.config.backup) self._publish_server_status()
pending_count = self.registry.pending_count()
write_status(self.config.storage.status_file, status, pending_count) def _publish_server_status(self) -> None:
if status.reachable: status = check_server(self.config.backup)
self.logger.info("Backup server reachable; pending files: %s", pending_count) pending_count = self.registry.pending_count()
else: write_status(self.config.storage.status_file, status, pending_count)
self.logger.warning( if status.reachable:
"Backup server unreachable: %s; pending files: %s", self.logger.info("Backup server reachable; pending files: %s", pending_count)
status.message, else:
pending_count, self.logger.warning(
) "Backup server unreachable: %s; pending files: %s",
status.message,
pending_count,
)
def _safe_size(path: Path) -> int | None: def _safe_size(path: Path) -> int | None:

View File

@@ -66,6 +66,7 @@ def test_migrate_legacy_rsync_config_to_robocopy_defaults() -> None:
assert data["backup"]["engine"] == "robocopy" assert data["backup"]["engine"] == "robocopy"
assert data["backup"]["robocopy_path"] == "robocopy" assert data["backup"]["robocopy_path"] == "robocopy"
assert data["backup"]["server_check"]["port"] == 445 assert data["backup"]["server_check"]["port"] == 445
assert data["backup"]["server_check"]["interval_seconds"] == 600
assert data["backup"]["remote_destinations"] == [] assert data["backup"]["remote_destinations"] == []