sxcli.devSimple Extensible CLI
You are viewing an archived snapshot (v0.2.0). Switch to the current version →

Windows services

An applet that should run under the Windows Service Control Manager implements one extra interface — and keeps working as a plain console program, on every platform, with the same configuration pipeline.

One applet, both launch modes#

go
go
// windows only (x/sys/windows/svc types)
type SCMApplet interface {
    Applet
    Execute(args []string, req <-chan svc.ChangeRequest,
        status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32)
}

SCMApplet extends Applet: started as a normal process, the framework drives Run() as usual; under the SCM it drives Execute instead. One applet, both launch modes — and console-mode debugging of the service comes for free.

Keep Execute in a _windows.go file: on other platforms the type simply remains a plain Applet, and the binary cross-compiles.

What the framework's handler does#

fw.Main() asks the platform layer whether the process is running under the SCM. If so, it calls svc.Run with a core-owned handler that:

  1. reports start-pending immediately, so the SCM does not kill the service during initialization — your Execute never worries about this;
  2. takes the argument vector from the SCM (this is where args come from in service mode — not os.Args);
  3. runs the standard pipeline — parse, resolve, configure, start — the same one a console run gets;
  4. delegates to your Execute, forwarding the SCM request/status channels so stop, shutdown and interrogate requests reach the applet;
  5. after Execute returns: reverse-order Stop of started services, then the final status to the SCM.

Failures map to the appropriate SCM status instead of a plain exit code. And a dispatched applet that does not implement SCMApplet while running under the SCM is a logged error, exit code 2 — not a hang.

What your Execute must do#

The framework brackets everything else, so the contract left to you is small:

  • send svc.Running (with the commands you accept) on the status channel once you are serving;
  • answer Interrogate by echoing the current status;
  • on Stop/Shutdown, report StopPending and return — the framework takes over from there (reverse-order Stop, final SCM status).

The Windows service demo shows a complete applet doing exactly this.

Console testing with --scm-debug#

The same handler can run outside the service manager: the Windows-only --scm-debug argument enters x/sys/windows/svc/debug mode — a console process where Ctrl+C and Ctrl+Break are translated into Stop and Shutdown requests, exercising the real Execute path.

It is default-off and deliberately hard to reach:

go
go
func main() {
    fw.Enable(fw.FeatureSCMDebug) // build-time opt-in
    fw.Main()
}

Without the opt-in the token is rejected as an unknown argument — as it is on every non-Windows platform. It is argument-only by construction (a platform-level pre-scan, never a config or env value) and absent from --help.

The framework's own test suite drives this path under Wine, so the service machinery is exercised in CI without a Windows box.

Configuration on Windows#

Nothing changes conceptually — the same struct, sources and precedence. The system config location becomes %ProgramData%\<applet>\config.<ext> and the user location %AppData%\<applet>\config.<ext>; .exe is stripped before argv[0] matching. Details in Config discovery & hardening.

The Windows-only API surface is documented in the fw (windows) reference.