feat: complete Windows client control and recovery paths
This commit is contained in:
+42
-5
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/rvbox/rvbox/internal/client/windowsservice"
|
||||
"github.com/rvbox/rvbox/internal/config"
|
||||
"github.com/rvbox/rvbox/internal/domain"
|
||||
"github.com/rvbox/rvbox/internal/observability"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
return errors.New("an internal mode is required (use --help)")
|
||||
}
|
||||
if args[0] == "--help" || args[0] == "-h" {
|
||||
_, err := io.WriteString(output, "usage: rvbox --service|--tray|--check-config|--install-service|--uninstall-service|--start-service|--stop-service --config PATH\n")
|
||||
_, err := io.WriteString(output, "usage: rvbox --service|--tray|--check-config|--install-service|--uninstall-service|--configure-service|--start-service|--stop-service|--restart-service --config PATH\n")
|
||||
return err
|
||||
}
|
||||
flags := flag.NewFlagSet("rvbox", flag.ContinueOnError)
|
||||
@@ -51,8 +52,11 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
checkConfig := flags.Bool("check-config", false, "validate client configuration and exit")
|
||||
install := flags.Bool("install-service", false, "install or update the machine-wide service")
|
||||
uninstall := flags.Bool("uninstall-service", false, "remove the machine-wide service")
|
||||
configure := flags.Bool("configure-service", false, "configure machine-wide service startup mode")
|
||||
startup := flags.String("startup", string(windowsservice.StartupAutomatic), "service startup mode: automatic or manual")
|
||||
start := flags.Bool("start-service", false, "start the machine-wide service")
|
||||
stop := flags.Bool("stop-service", false, "stop the machine-wide service")
|
||||
restart := flags.Bool("restart-service", false, "restart the machine-wide service")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,7 +64,7 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
return fmt.Errorf("unexpected argument %q", flags.Arg(0))
|
||||
}
|
||||
selected := 0
|
||||
for _, value := range []bool{*serviceMode, *trayMode, *checkConfig, *install, *uninstall, *start, *stop} {
|
||||
for _, value := range []bool{*serviceMode, *trayMode, *checkConfig, *install, *uninstall, *configure, *start, *stop, *restart} {
|
||||
if value {
|
||||
selected++
|
||||
}
|
||||
@@ -69,10 +73,11 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
return errors.New("select exactly one rvbox mode")
|
||||
}
|
||||
if *checkConfig {
|
||||
if _, err := loadClientConfig(*configPath); err != nil {
|
||||
configured, err := loadClientConfig(*configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := fmt.Fprintf(output, "valid client configuration: %s\n", *configPath)
|
||||
_, err = fmt.Fprintf(output, "valid client configuration: path=%s server_url=%s state_dir=%s daemon_cwd=%s observability_listen=%s\n", *configPath, configured.Client.ServerURL, configured.Client.StateDir, configured.Client.DaemonCWD, configured.Observability.Listen)
|
||||
return err
|
||||
}
|
||||
if *install {
|
||||
@@ -85,12 +90,18 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
if *uninstall {
|
||||
return windowsservice.Uninstall()
|
||||
}
|
||||
if *configure {
|
||||
return windowsservice.Configure(windowsservice.StartupMode(*startup))
|
||||
}
|
||||
if *start {
|
||||
return windowsservice.Start()
|
||||
}
|
||||
if *stop {
|
||||
return windowsservice.Stop(30)
|
||||
}
|
||||
if *restart {
|
||||
return windowsservice.Restart(30)
|
||||
}
|
||||
if *trayMode {
|
||||
return runTray(*configPath, diagnostics)
|
||||
}
|
||||
@@ -116,8 +127,15 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
health := observability.New()
|
||||
go func() {
|
||||
if serveErr := health.Serve(ctx, configured.Observability.Listen, observability.Paths{Liveness: configured.Observability.LivenessPath, Readiness: configured.Observability.ReadinessPath, Metrics: configured.Observability.MetricsPath}); serveErr != nil && ctx.Err() == nil && diagnostics != nil {
|
||||
_, _ = fmt.Fprintf(diagnostics, "rvbox client observability endpoint stopped: %v\n", serveErr)
|
||||
}
|
||||
}()
|
||||
state, err := spool.Open(ctx, spool.Options{DataDir: configured.Client.StateDir, BusyTimeout: 5 * time.Second, TombstoneLimit: configured.Storage.TombstoneMaxEntries, MaxScriptBytes: configured.Execution.MaxScriptBytes, MaxExecutionSpecBytes: configured.Execution.MaxExecutionSpecBytes, QuotaLimits: spool.QuotaLimits{HardAllocationBytes: 1 << 20, CommandOutputBytes: configured.Storage.CommandOutputLimitBytes, CommandTotalBytes: configured.Storage.CommandTotalLimitBytes, ClientTotalBytes: configured.Storage.ClientTotalLimitBytes, CloseoutReserveBytes: configured.Storage.CommandCloseoutReserveBytes}})
|
||||
if err != nil {
|
||||
health.SetDirty(true)
|
||||
return fmt.Errorf("open client durable state: %w", err)
|
||||
}
|
||||
defer state.Close()
|
||||
@@ -134,7 +152,7 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
limits = agentproto.DefaultLimits()
|
||||
}
|
||||
eventReady := make(chan domain.UUID, 256)
|
||||
supervised, err := clientwindows.NewSupervisor(clientwindows.NativeOptions{Shells: clientwindows.ShellPaths{CMD: configured.Shells.CMD, PowerShell: configured.Shells.PowerShell}, WorkRoot: configured.Client.DaemonCWD, MaxWrapperBytes: configured.Execution.MaxScriptBytes, MaxOutputChunk: configured.Execution.MaxRawChunkBytes, WindowsTermGrace: configured.Execution.WindowsTermGrace})
|
||||
supervised, err := clientwindows.NewSupervisor(clientwindows.NativeOptions{Shells: clientwindows.ShellPaths{CMD: configured.Shells.CMD, PowerShell: configured.Shells.PowerShell}, WorkRoot: configured.Client.DaemonCWD, JobProfiles: clientJobProfiles(configured.Profiles), MaxWrapperBytes: configured.Execution.MaxScriptBytes, MaxOutputChunk: configured.Execution.MaxRawChunkBytes, WindowsTermGrace: configured.Execution.WindowsTermGrace})
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure command supervisor: %w", err)
|
||||
}
|
||||
@@ -149,12 +167,14 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
}
|
||||
runner := func() {
|
||||
if _, checkErr := state.Check(ctx); checkErr != nil {
|
||||
health.SetDirty(true)
|
||||
if diagnostics != nil {
|
||||
_, _ = fmt.Fprintf(diagnostics, "rvbox client spool is dirty: %v\n", checkErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if recovered, recoverErr := state.RecoverLaunchUncertainty(ctx, time.Now().UTC()); recoverErr != nil {
|
||||
health.SetDirty(true)
|
||||
if diagnostics != nil {
|
||||
_, _ = fmt.Fprintf(diagnostics, "rvbox launch recovery failed: %v\n", recoverErr)
|
||||
}
|
||||
@@ -162,6 +182,7 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
} else if len(recovered) > 0 && diagnostics != nil {
|
||||
_, _ = fmt.Fprintf(diagnostics, "rvbox recovered %d uncertain launch(es)\n", len(recovered))
|
||||
}
|
||||
health.SetReady(true)
|
||||
if runErr := agent.Run(ctx, agent.RunnerOptions{
|
||||
Store: state,
|
||||
Dial: func(dialContext context.Context) (agent.Transport, error) {
|
||||
@@ -182,6 +203,22 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientJobProfiles(profiles config.Profiles) map[string]clientwindows.JobProfile {
|
||||
return map[string]clientwindows.JobProfile{
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_LIGHT.String(): toJobProfile(profiles.Light),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_CPU_MEDIUM.String(): toJobProfile(profiles.CPUMedium),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_CPU_HEAVY.String(): toJobProfile(profiles.CPUHeavy),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_MEM_MEDIUM.String(): toJobProfile(profiles.MemMedium),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_MEM_HEAVY.String(): toJobProfile(profiles.MemHeavy),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_DISK_MEDIUM.String(): toJobProfile(profiles.DiskMedium),
|
||||
rvboxv1.ExecutionProfile_EXECUTION_PROFILE_DISK_HEAVY.String(): toJobProfile(profiles.DiskHeavy),
|
||||
}
|
||||
}
|
||||
|
||||
func toJobProfile(profile config.Profile) clientwindows.JobProfile {
|
||||
return clientwindows.JobProfile{RequiredControls: append([]string(nil), profile.RequiredControls...), CPUPercent: profile.CPUPercent, MemoryMaxBytes: profile.MemoryMaxBytes, PIDsMax: profile.PIDsMax, IOReadBPS: profile.WindowsIOReadBPS, IOWriteBPS: profile.WindowsIOWriteBPS}
|
||||
}
|
||||
|
||||
func clientHTTPClient(settings config.TLS) (*http.Client, error) {
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, ServerName: settings.ServerName} // #nosec G402 -- TLS 1.2 is the v1 floor.
|
||||
if settings.CAFile != "" {
|
||||
|
||||
Reference in New Issue
Block a user