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. 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 ```text
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: Il notifier puo' essere eseguito da Task Scheduler con:
```powershell ```powershell
python -m bakrest.server_notifier python -m bakrest.server_notifier
@@ -66,7 +68,7 @@ Argomenti: -m bakrest.server_notifier
Avvia in: C:\devel\bak&rest 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 ## Backup senza GUI per Task Scheduler
@@ -115,9 +117,29 @@ robocopy_path = "robocopy"
[backup.server_check] [backup.server_check]
type = "tcp" type = "tcp"
port = 445 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: 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 ```text

View File

@@ -34,10 +34,9 @@ Il servizio Windows deve:
- applicare esclusioni di directory e pattern; - applicare esclusioni di directory e pattern;
- registrare i file cambiati in un registro persistente; - registrare i file cambiati in un registro persistente;
- mantenere un log giornaliero delle operazioni; - mantenere un log giornaliero delle operazioni;
- verificare periodicamente la raggiungibilita' del server di backup; - mantenere un log giornaliero delle operazioni.
- segnalare lo stato del server alla tray app.
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. 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 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 ## Strategia di monitoraggio

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 = 600 interval_seconds = 1800
timeout_seconds = 3 timeout_seconds = 3
[stability] [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: 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: if isinstance(server_check, dict) and int(server_check.get("interval_seconds", 300)) in {300, 600}:
server_check["interval_seconds"] = 600 server_check["interval_seconds"] = 1800
changed = True changed = True
destinations = backup.get("remote_destinations") destinations = backup.get("remote_destinations")

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 = 600 interval_seconds = 1800
timeout_seconds = 3 timeout_seconds = 3
[stability] [stability]

View File

@@ -2,14 +2,12 @@ from __future__ import annotations
import logging import logging
import threading import threading
import time
from pathlib import Path from pathlib import Path
from .config import AppConfig from .config import AppConfig
from .filters import WatchFilter from .filters import WatchFilter
from .logging_setup import close_logger from .logging_setup import close_logger
from .registry import ChangeEvent, ChangeRegistry from .registry import ChangeEvent, ChangeRegistry
from .server_check import check_server, write_status
class WatchdogRuntime: class WatchdogRuntime:
@@ -49,7 +47,7 @@ class WatchdogRuntime:
observer.start() observer.start()
self.logger.info("BakRestWatchdog started") self.logger.info("BakRestWatchdog started")
try: try:
self._run_health_loop() self._run_service_loop()
finally: finally:
observer.stop() observer.stop()
observer.join(timeout=10) observer.join(timeout=10)
@@ -82,24 +80,9 @@ class WatchdogRuntime:
self.registry.record(change) self.registry.record(change)
self.logger.info("Recorded %s: %s", event_type, target_path) self.logger.info("Recorded %s: %s", event_type, target_path)
def _run_health_loop(self) -> None: def _run_service_loop(self) -> None:
interval = self.config.backup.server_check.interval_seconds while not self.stop_event.wait(60):
self._publish_server_status() pass
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 _safe_size(path: Path) -> int | None: 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"]["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"]["server_check"]["interval_seconds"] == 1800
assert data["backup"]["remote_destinations"] == [] 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