d29475560d
internal/agent/service: build-tagged into service_windows.go (svc.Handler that listens for Stop/Shutdown + delegates to the agent loop) and service_other.go (foreground stub for Linux/macOS). install_windows.go wraps mgr.Connect+CreateService/Delete/Start/Stop for the new 'restic-manager-agent install|uninstall|start|stop' subcommands. Cross-compile verified: GOOS=windows GOARCH=amd64 go build ./cmd/agent succeeds. UNTESTED on Windows itself — the SCM round-trip can't be exercised from Linux CI; treat as a starting point for the first real Windows install.
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
//go:build windows
|
|
|
|
// service_windows.go — Service Control Manager integration for the
|
|
// agent on Windows (P2-16). Implements the svc.Handler interface so
|
|
// `restic-manager-agent run` works under both interactive and SCM
|
|
// contexts. install/uninstall live in install_windows.go.
|
|
//
|
|
// UNTESTED on Windows in this repo's CI (the runners are Linux).
|
|
// The shape mirrors the canonical example in
|
|
// golang.org/x/sys/windows/svc/example. Treat any deviation from
|
|
// that example as suspicious.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"golang.org/x/sys/windows/svc"
|
|
)
|
|
|
|
// ServiceName is the SCM identifier for the agent service.
|
|
const ServiceName = "restic-manager-agent"
|
|
|
|
// AgentRun is the function the service handler calls to start the
|
|
// agent's main loop. Pass cmd/agent's run-loop entry point at the
|
|
// call site so this package stays free of cross-cmd imports.
|
|
type AgentRun func(ctx context.Context) error
|
|
|
|
// Run delegates to the SCM dispatcher when running under Windows
|
|
// service control, otherwise runs the agent loop in the foreground
|
|
// (for `restic-manager-agent run` from a console, e.g. while
|
|
// debugging on a developer's box).
|
|
func Run(agentRun AgentRun) error {
|
|
isService, err := svc.IsWindowsService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isService {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
return agentRun(ctx)
|
|
}
|
|
return svc.Run(ServiceName, &handler{run: agentRun})
|
|
}
|
|
|
|
// handler implements svc.Handler. Execute is called once when the
|
|
// service is started. We spawn the agent loop in a goroutine and
|
|
// listen for SCM Stop / Shutdown notifications, cancelling the
|
|
// context to wind down cleanly.
|
|
type handler struct {
|
|
run AgentRun
|
|
}
|
|
|
|
func (h *handler) Execute(_ []string, req <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
|
|
const accepted = svc.AcceptStop | svc.AcceptShutdown
|
|
status <- svc.Status{State: svc.StartPending}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
doneCh := make(chan error, 1)
|
|
go func() {
|
|
doneCh <- h.run(ctx)
|
|
}()
|
|
status <- svc.Status{State: svc.Running, Accepts: accepted}
|
|
|
|
for {
|
|
select {
|
|
case c := <-req:
|
|
switch c.Cmd {
|
|
case svc.Interrogate:
|
|
status <- c.CurrentStatus
|
|
case svc.Stop, svc.Shutdown:
|
|
slog.Info("svc: stop requested")
|
|
cancel()
|
|
status <- svc.Status{State: svc.StopPending}
|
|
if err := <-doneCh; err != nil && !errors.Is(err, context.Canceled) {
|
|
slog.Warn("svc: agent loop exited with error", "err", err)
|
|
return false, 1
|
|
}
|
|
return false, 0
|
|
}
|
|
case err := <-doneCh:
|
|
// Agent loop exited on its own — uncommon (only via signal
|
|
// or fatal error). Surface as an SCM stop.
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
slog.Warn("svc: agent loop exited unexpectedly", "err", err)
|
|
return false, 1
|
|
}
|
|
return false, 0
|
|
}
|
|
}
|
|
}
|