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>
160 lines
5.6 KiB
Go
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)))
|
|
}
|
|
}
|
|
}
|
|
}
|