Files

127 lines
3.8 KiB
Go

// 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) }