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#
// 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:
- reports start-pending immediately, so the SCM does not kill the
service during initialization — your
Executenever worries about this; - takes the argument vector from the SCM (this is where args come from
in service mode — not
os.Args); - runs the standard pipeline — parse, resolve, configure, start — the same one a console run gets;
- delegates to your
Execute, forwarding the SCM request/status channels so stop, shutdown and interrogate requests reach the applet; - after
Executereturns: reverse-orderStopof 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
Interrogateby echoing the current status; - on
Stop/Shutdown, reportStopPendingand return — the framework takes over from there (reverse-orderStop, 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:
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.