Aggiunge task scheduler XML

This commit is contained in:
2026-07-01 12:58:58 +02:00
parent 6232bb240c
commit 5afcdd128b
10 changed files with 170 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Author>BakRest</Author>
<Description>Run Bak&amp;Rest no-GUI backup when the user logs off, then shut down on success.</Description>
</RegistrationInfo>
<Triggers>
<EventTrigger>
<Enabled>true</Enabled>
<Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select Path="Security"&gt;*[System[(EventID=4647)]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
</EventTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT4H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>C:\Python314\python.exe</Command>
<Arguments>-m bakrest.tray_app --nogui</Arguments>
<WorkingDirectory>C:\devel\bak&amp;rest</WorkingDirectory>
</Exec>
</Actions>
</Task>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Author>BakRest</Author>
<Description>Check the Bak&amp;Rest backup server every 30 minutes and notify the user if it is unreachable.</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
</LogonTrigger>
<CalendarTrigger>
<StartBoundary>2026-07-01T08:00:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
<Repetition>
<Interval>PT30M</Interval>
<Duration>P1D</Duration>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT5M</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>C:\Python314\python.exe</Command>
<Arguments>-m bakrest.server_notifier</Arguments>
<WorkingDirectory>C:\devel\bak&amp;rest</WorkingDirectory>
</Exec>
</Actions>
</Task>

View File

@@ -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"] == []

31
tests/test_task_xml.py Normal file
View File

@@ -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 "<Interval>PT30M</Interval>" 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