//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))) } } } }