syncova-backup/apps/agent/cmd/syncova-agent/service_windows.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

160 lines
5.6 KiB
Go

//go:build windows
package main
import (
"context"
"log/slog"
"time"
"github.com/syncova/syncova/packages/agent"
"golang.org/x/sys/windows/svc"
)
// serviceName ist der Name des Windows-Dienstes.
//
// Er muss mit dem Namen übereinstimmen, unter dem der Dienst registriert wurde
// (`sc.exe create Syncova...`). Weicht er ab, meldet sich der Prozess beim
// Dienstverwalter nicht an, und Windows beendet ihn nach der Startfrist.
const windowsServiceName = "SyncovaAgent"
// serviceStopTimeout begrenzt das geordnete Beenden.
//
// Windows räumt einem Dienst standardmäßig etwa 30 Sekunden ein, bevor es ihn
// hart beendet. Der Agent meldet deshalb frühzeitig „wird beendet" und bricht
// eine laufende Sicherung ab, statt sie zu Ende zu führen: Ein Neustart, der am
// längsten Backup hängt, ist schlimmer als ein abgebrochener Lauf — und ein
// abgebrochener hinterlässt dank des Commit-Protokolls kein sichtbares Backup.
const serviceStopTimeout = 20 * time.Second
// runUnderServiceManager startet den Agent unter Windows.
//
// **Ungeprüfter Code.** Diese Umsetzung wurde auf einem macOS-System
// geschrieben. Sie übersetzt für Windows (`make cross-build` prüft das bei jedem
// Lauf), wurde dort aber nie ausgeführt. Vor einem produktiven Einsatz ist auf
// einem Windows-System zu prüfen:
//
// - die Anmeldung am Dienstverwalter und der Startzeitpunkt
// - das Verhalten bei „Dienst beenden" und beim Systemneustart
// - die Rechte des Dienstkontos beim Lesen der zu sichernden Dateien
//
// Läuft der Prozess nicht unter dem Dienstverwalter — etwa beim Aufruf von
// Hand —, arbeitet er im Vordergrund weiter. Das ist keine Notlösung, sondern
// der übliche Weg: So lässt sich derselbe Aufruf für einen Probelauf und für
// den Dienstbetrieb verwenden.
func runUnderServiceManager(runContext context.Context, agentRunner *agent.Runner, agentLogger *slog.Logger) error {
isWindowsService, detectionError := svc.IsWindowsService()
if detectionError != nil {
// Die Erkennung schlägt fehl — das ist kein Grund, den Agenten nicht zu
// starten. Er läuft dann im Vordergrund, und die Meldung sagt warum.
agentLogger.Warn("die dienstumgebung liess sich nicht bestimmen; der agent laeuft im vordergrund",
slog.String("grund", detectionError.Error()))
return agentRunner.Run(runContext)
}
if !isWindowsService {
agentLogger.Info("der agent laeuft im vordergrund (kein dienstkontext)")
return agentRunner.Run(runContext)
}
agentLogger.Info("der agent meldet sich am dienstverwalter an",
slog.String("dienst", windowsServiceName))
return svc.Run(windowsServiceName, &windowsServiceHandler{
runContext: runContext,
agentRunner: agentRunner,
agentLogger: agentLogger,
})
}
// windowsServiceHandler verbindet den Agenten mit dem Dienstverwalter.
type windowsServiceHandler struct {
// runContext bricht den Agenten von außen ab.
runContext context.Context
// agentRunner ist die Betriebsschleife.
agentRunner *agent.Runner
// agentLogger protokolliert den Verlauf.
agentLogger *slog.Logger
}
// Execute bedient den Dienstverwalter.
//
// Der Ablauf ist von Windows vorgegeben: erst „startet", dann „läuft" melden,
// danach auf Steuerbefehle warten. Wer den Zustand „läuft" nicht innerhalb der
// Startfrist meldet, wird vom Dienstverwalter beendet — mit einer Meldung, die
// wie ein Absturz aussieht.
func (handler *windowsServiceHandler) Execute(serviceArguments []string,
changeRequests <-chan svc.ChangeRequest, statusUpdates chan<- svc.Status) (bool, uint32) {
const acceptedCommands = svc.AcceptStop | svc.AcceptShutdown
statusUpdates <- svc.Status{State: svc.StartPending}
// Der Agent läuft in einem eigenen Ablauf; dieser hier muss für den
// Dienstverwalter ansprechbar bleiben.
serviceContext, stopAgent := context.WithCancel(handler.runContext)
defer stopAgent()
agentFinished := make(chan error, 1)
go func() {
agentFinished <- handler.agentRunner.Run(serviceContext)
}()
statusUpdates <- svc.Status{State: svc.Running, Accepts: acceptedCommands}
for {
select {
case runError := <-agentFinished:
// Der Agent hat von sich aus geendet — etwa weil sein Token
// abgelehnt wurde. Das ist ein Dienstfehler, kein geordnetes Ende.
if runError != nil {
handler.agentLogger.Error("der agent hat sich beendet",
slog.String("grund", runError.Error()))
statusUpdates <- svc.Status{State: svc.Stopped}
// Ein von null verschiedener Code lässt Windows den Dienst als
// fehlerhaft führen — und, je nach Einstellung, neu starten.
return false, 1
}
statusUpdates <- svc.Status{State: svc.Stopped}
return false, 0
case changeRequest := <-changeRequests:
switch changeRequest.Cmd {
case svc.Interrogate:
// Der Dienstverwalter fragt den Zustand ab. Die Antwort ist
// der unveränderte aktuelle Zustand.
statusUpdates <- changeRequest.CurrentStatus
case svc.Stop, svc.Shutdown:
handler.agentLogger.Info("der dienstverwalter beendet den agenten")
statusUpdates <- svc.Status{State: svc.StopPending}
stopAgent()
// Auf das Ende warten, aber nicht endlos: Windows beendet den
// Prozess sonst hart, und ein hart beendeter Dienst erscheint
// im Ereignisprotokoll als Absturz.
select {
case <-agentFinished:
case <-time.After(serviceStopTimeout):
handler.agentLogger.Warn("der agent endete nicht innerhalb der frist")
}
statusUpdates <- svc.Status{State: svc.Stopped}
return false, 0
default:
handler.agentLogger.Warn("unerwarteter steuerbefehl des dienstverwalters",
slog.Int("befehl", int(changeRequest.Cmd)))
}
}
}
}