// Package windowsservice owns the machine-wide Windows service contract. The // policy and transition model are platform-neutral so they can be exercised // on Linux; service manager calls live in build-tagged adapters. package windowsservice import ( "context" "errors" "fmt" "strings" winlaunch "github.com/rvbox/rvbox/internal/client/supervisor/windows" ) const ( Name = "RVBoxClient" DisplayName = "RVBox Client" Description = "RVBox Windows client daemon and command supervisor" ) var ( ErrInvalidInstallSpec = errors.New("invalid Windows service install specification") ErrUnsupported = errors.New("Windows service management is unavailable on this platform") ErrInvalidTransition = errors.New("invalid Windows service lifecycle transition") ) type StartupMode string const ( StartupAutomatic StartupMode = "automatic" StartupManual StartupMode = "manual" ) // InstallSpec is the complete immutable service image contract. The service // is always one machine-wide LocalSystem process; config selection is explicit // and never delegated to PATH, the current directory, or Task Scheduler. type InstallSpec struct { ExecutablePath string ConfigPath string Startup StartupMode } func (spec InstallSpec) Validate() error { if !winlaunch.ValidAbsoluteWindowsPath(spec.ExecutablePath) || !winlaunch.ValidAbsoluteWindowsPath(spec.ConfigPath) { return ErrInvalidInstallSpec } if strings.ContainsRune(spec.ExecutablePath, 0) || strings.ContainsRune(spec.ConfigPath, 0) { return ErrInvalidInstallSpec } if spec.Startup != StartupAutomatic && spec.Startup != StartupManual { return ErrInvalidInstallSpec } return nil } func (spec InstallSpec) String() string { return fmt.Sprintf("%s config=%s startup=%s", spec.ExecutablePath, spec.ConfigPath, spec.Startup) } type State uint8 const ( StateUnknown State = iota StateStopped StateStartPending StateRunning StateStopPending StatePaused ) type Command uint8 const ( CommandStart Command = iota + 1 CommandStop CommandRestart ) // Transition validates only state changes that the service control adapter is // allowed to request. Pending states are intentionally not collapsed into a // successful result: callers must poll SCM and observe the final state. func Transition(state State, command Command) (State, error) { switch command { case CommandStart: switch state { case StateStopped: return StateStartPending, nil case StateRunning, StateStartPending: return state, nil default: return StateUnknown, ErrInvalidTransition } case CommandStop: switch state { case StateRunning, StatePaused: return StateStopPending, nil case StateStopped, StateStopPending: return state, nil default: return StateUnknown, ErrInvalidTransition } case CommandRestart: switch state { case StateStopped: return StateStartPending, nil case StateRunning, StatePaused: return StateStopPending, nil case StateStartPending, StateStopPending: return state, nil default: return StateUnknown, ErrInvalidTransition } default: return StateUnknown, ErrInvalidTransition } } // Install, Uninstall, Start, and Stop are intentionally narrow. Their // platform implementations return ErrUnsupported on non-Windows builds. func Install(spec InstallSpec) error { return installNative(spec) } func Uninstall() error { return uninstallNative() } func Configure(startup StartupMode) error { return configureNative(startup) } func Start() error { return startNative() } func Stop(timeoutSeconds uint32) error { return stopNative(timeoutSeconds) } func Restart(timeoutSeconds uint32) error { return restartNative(timeoutSeconds) } func Run(run func(context.Context) error) error { return runNative(run) }