Aggiunge task scheduler XML
This commit is contained in:
30
README.md
30
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
45
tasks/BakRestBackupOnLogoff.xml
Normal file
45
tasks/BakRestBackupOnLogoff.xml
Normal 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&Rest no-GUI backup when the user logs off, then shut down on success.</Description>
|
||||||
|
</RegistrationInfo>
|
||||||
|
<Triggers>
|
||||||
|
<EventTrigger>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<Subscription><QueryList><Query Id="0" Path="Security"><Select Path="Security">*[System[(EventID=4647)]]</Select></Query></QueryList></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&rest</WorkingDirectory>
|
||||||
|
</Exec>
|
||||||
|
</Actions>
|
||||||
|
</Task>
|
||||||
56
tasks/BakRestServerNotifierEvery30Minutes.xml
Normal file
56
tasks/BakRestServerNotifierEvery30Minutes.xml
Normal 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&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&rest</WorkingDirectory>
|
||||||
|
</Exec>
|
||||||
|
</Actions>
|
||||||
|
</Task>
|
||||||
@@ -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
31
tests/test_task_xml.py
Normal 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
|
||||||
Reference in New Issue
Block a user