feat: complete Windows client control and recovery paths

This commit is contained in:
2026-09-06 13:37:32 +00:00
parent 56b15c7f4f
commit 486894557d
38 changed files with 2188 additions and 106 deletions
+71 -1
View File
@@ -12,10 +12,13 @@ import (
"os"
"os/signal"
"syscall"
"time"
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
"github.com/rvbox/rvbox/internal/agentproto"
"github.com/rvbox/rvbox/internal/config"
"github.com/rvbox/rvbox/internal/domain"
"github.com/rvbox/rvbox/internal/observability"
"github.com/rvbox/rvbox/internal/server/control"
"github.com/rvbox/rvbox/internal/server/session"
"github.com/rvbox/rvbox/internal/server/store"
@@ -24,12 +27,26 @@ import (
func main() {
var configPath string
var checkConfig bool
flag.StringVar(&configPath, "config", "", "absolute server TOML configuration path")
flag.BoolVar(&checkConfig, "check-config", false, "validate server configuration and exit")
flag.Parse()
if configPath == "" {
log.Print("rvbox-server: --config is required")
os.Exit(2)
}
if checkConfig {
configured, err := config.LoadServer(configPath)
if err != nil {
log.Printf("rvbox-server: invalid configuration: %v", err)
os.Exit(1)
}
if _, err := fmt.Fprintf(os.Stdout, "valid server configuration: data_dir=%s agent_listen=%s control_socket=%s observability_listen=%s json_rpc_enabled=%t\n", configured.Server.DataDir, configured.Server.AgentListen, configured.Server.ControlSocket, configured.Observability.Listen, configured.JSONRPC.Enabled); err != nil {
log.Printf("rvbox-server: write check result: %v", err)
os.Exit(1)
}
return
}
if err := run(configPath); err != nil {
log.Printf("rvbox-server: %v", err)
os.Exit(1)
@@ -58,6 +75,43 @@ func run(configPath string) error {
return err
}
defer persistence.Close()
health := observability.New()
healthServer := &http.Server{Handler: health.Handler(observability.Paths{Liveness: configured.Observability.LivenessPath, Readiness: configured.Observability.ReadinessPath, Metrics: configured.Observability.MetricsPath}), ReadHeaderTimeout: configured.Flow.WriteDeadline}
// Health is deliberately best effort. A port collision or a temporary
// listener failure must not prevent the control/agent service from starting;
// operators still get the failure in the daemon log.
var healthListener net.Listener
if configured.Observability.Listen != "" {
healthListener, err = net.Listen("tcp", configured.Observability.Listen)
if err != nil {
log.Printf("rvbox-server: observability endpoint unavailable (continuing without it): %v", err)
}
}
if healthListener != nil {
defer healthListener.Close()
}
recoveryContext, cancelRecovery := context.WithCancel(context.Background())
defer cancelRecovery()
// Recovery is deliberately asynchronous: liveness and incident inspection
// remain available while committed-range checks run. Readiness becomes
// true only after the real SQLite/segment recovery completes.
go func() {
if _, recoverErr := persistence.RecoverCommandSegments(recoveryContext); recoverErr != nil {
health.SetDirty(true)
if incidentID, idErr := domain.NewUUIDv7(); idErr == nil {
if _, incidentErr := persistence.RecordIncident(context.Background(), store.IncidentInput{
IncidentUUID: incidentID, DetectedAt: time.Now().UTC(), Kind: store.IncidentSQLiteIntegrity,
Scope: store.IncidentScopeGlobal, ScopeKey: "server-startup-recovery", Summary: "startup storage recovery failed",
Evidence: []byte(recoverErr.Error()), AutomaticallyRepairable: false,
}); incidentErr != nil {
log.Printf("rvbox-server: could not persist recovery incident: %v", incidentErr)
}
}
log.Printf("rvbox-server: storage recovery left readiness disabled: %v", recoverErr)
return
}
health.SetReady(true)
}()
listener, err := net.Listen("tcp", configured.Server.AgentListen)
if err != nil {
@@ -106,9 +160,12 @@ func run(configPath string) error {
HeartbeatIdle: configured.Protocol.HeartbeatIdle, LivenessTimeout: configured.Protocol.LivenessTimeout,
}
httpServer := &http.Server{Handler: agent, ReadHeaderTimeout: configured.Flow.WriteDeadline}
serveError := make(chan error, 3)
serveError := make(chan error, 4)
go func() { serveError <- httpServer.Serve(listener) }()
go func() { serveError <- grpcServer.Serve(controlListener) }()
if healthListener != nil {
go func() { serveError <- healthServer.Serve(healthListener) }()
}
if rpcServer != nil {
go func() { serveError <- rpcServer.Serve(rpcListener) }()
}
@@ -119,6 +176,9 @@ func run(configPath string) error {
select {
case err := <-serveError:
if errors.Is(err, http.ErrServerClosed) {
if healthListener != nil {
_ = healthServer.Close()
}
if rpcServer != nil {
_ = rpcServer.Close()
}
@@ -126,6 +186,9 @@ func run(configPath string) error {
return nil
}
_ = httpServer.Close()
if healthListener != nil {
_ = healthServer.Close()
}
if rpcServer != nil {
_ = rpcServer.Close()
}
@@ -135,6 +198,13 @@ func run(configPath string) error {
shutdownContext, cancel := context.WithTimeout(context.Background(), configured.Server.ShutdownGrace)
defer cancel()
httpErr := httpServer.Shutdown(shutdownContext)
var healthErr error
if healthListener != nil {
healthErr = healthServer.Shutdown(shutdownContext)
}
if httpErr == nil {
httpErr = healthErr
}
if rpcServer != nil {
if err := rpcServer.Shutdown(shutdownContext); httpErr == nil {
httpErr = err