Skip to content

Run satpulsed as a Windows service - #407

Open
jclark wants to merge 5 commits into
masterfrom
windows-service
Open

Run satpulsed as a Windows service#407
jclark wants to merge 5 commits into
masterfrom
windows-service

Conversation

@jclark

@jclark jclark commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Lets satpulsed run as a Windows service: an SCM-managed background process that starts at boot and stops cleanly on a service stop. This is the last piece of the Windows port on top of the compile port, serial I/O, and CI work that already landed. The design is plan/windows-svc.md (archived here).

The first commit is a behaviour-preserving refactor: daemon.Cmd becomes daemon.Run(ctx, cfg, lg), with flag parsing, config loading, logger construction, signal handling, and process exit moved up into cmd/satpulsed. The service path needs a different logger sink (no console) and a different cancellation source (the SCM control channel rather than signals), which is exactly what the caller should own. A plain context cancellation is the normal stop path and returns nil; a non-Canceled cancellation cause (such as the scan worker reporting lost serial input) is promoted to the returned error as before.

The rest adds the Windows-only service wrapper, dispatched before the foreground path. --register and --unregister manage only the SCM registration; they never delete files, so unregistering cannot trip over Windows' inability to delete a running image, and no install-state bookkeeping is needed.

  • --register registers the service (start type automatic) and its Event Log source, baking --win-svc, the config file, and --log-file into the registered command as absolute paths, since the SCM launches services from a different working directory. By default the running executable is registered in place, which suits an unpacked release zip. With --copy <dir> the executable is first copied there and the copy registered, so a running service does not lock the development binary; registration checks for an existing service before copying and removes the copy if a later step fails.
  • --unregister stops the service if needed and unregisters it and the Event Log source. It deletes no files: when the registered executable is not the one running, its path is printed so the user can remove it. It requires no config file, so a broken or deleted config does not prevent unregistration.
  • --win-svc runs the svc.Run handler, mapping SCM Stop/Shutdown onto the context cancellation that daemon.Run treats as a clean stop. Config loading and the --log-file logger are set up inside Execute, so a bad config is reported to the SCM as StartPending -> Stopped with an Event Log entry instead of a silent pre-service exit. Failures are reported with a service-specific exit code, since the SCM would misread the sysexits-style codes as Win32 error numbers.

Logging is split between two sinks: the daemon's slog stream goes to the --log-file in service mode, while the Windows Event Log carries only the service-lifecycle events and the terminal error, written by the wrapper; the daemon package stays Windows-unaware. On other platforms the new hooks are no-ops and behaviour is unchanged.

Closes #338.

jclark added 3 commits August 11, 2026 11:35
daemon.Cmd owned flag parsing, config loading, logger construction,
signal handling, and os.Exit, which tied the daemon to a console
foreground process. The Windows service mode needs a different logger
sink and a different cancellation source (the SCM control channel, not
signals), so those concerns belong to the caller.

Replace Cmd with Run(ctx, cfg, lg), which runs the daemon until ctx is
cancelled and returns the settled error; a plain context cancellation is
the normal stop path and returns nil, while a non-Canceled cancellation
cause (such as the scan worker reporting lost serial input) is promoted
to the returned error exactly as before. Move flag parsing into
cmd/satpulsed, along with the mandatory-config check, which now happens
outside the parser so future run modes can decide it for themselves.
Export ExitCode, ExitUsage, ExitConfig, and ConfigFileEnvVar for use by
cmd/satpulsed. Behaviour on the foreground path is unchanged.

This is step 1 of plan/windows-svc.md (#338).
satpulsed built and ran in the foreground on Windows, but there was no
way to run it as an SCM-managed background process that starts at boot
and stops cleanly on a service stop.

Add Windows-only service support to cmd/satpulsed, dispatched before the
foreground path:

- --install registers the service (start type automatic), baking
  --win-svc, the config file, and --log-file into the registered command
  as absolute paths, since the SCM launches services from a different
  working directory. With --install-dir the executable is first copied
  there and the copy registered, so the running service does not lock
  the development binary; what install created is recorded under the
  service registry Parameters key.
- --uninstall stops the service if needed, unregisters it and the Event
  Log source, and removes only what install recorded. It requires no
  config file, so a broken or deleted config does not prevent removal.
- --win-svc runs the svc.Run handler, mapping SCM Stop/Shutdown onto the
  context cancellation that daemon.Run already treats as a clean stop.
  Config loading and the --log-file logger are set up inside Execute, so
  a bad config is reported to the SCM as StartPending -> Stopped with an
  Event Log entry instead of a silent pre-service exit. Failures return
  a service-specific exit code, since the SCM would misread the
  sysexits-style codes as Win32 error numbers.

The daemon slog stream goes to --log-file in service mode; the Windows
Event Log carries only the service-lifecycle events and the terminal
error, written by the wrapper. A !windows counterpart keeps the
platform hooks no-ops elsewhere.

Fixes #338
The Windows service support it describes is implemented.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38f6c752b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/satpulsed/svc_windows.go Outdated
Comment on lines +315 to +316
if err := os.Remove(copiedExe); err != nil && !os.IsNotExist(err) {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid deleting the currently running installed executable

When --install-dir was used and an administrator later invokes --uninstall using that installed copy, copiedExe refers to the currently running executable. Windows does not permit removing a running executable, so this returns an error after the service and Event Log source have already been deleted, leaving the copied binary and directory behind; once the service registry key disappears, a retry also loses the ownership metadata needed for cleanup. Handle self-uninstallation explicitly rather than removing the active image synchronously.

Useful? React with 👍 / 👎.

Comment thread cmd/satpulsed/svc_windows.go Outdated
Comment on lines +220 to +222
copiedExe = filepath.Join(dir, "satpulsed.exe")
if err := copyFile(exe, copiedExe); err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer copying files until installation preflight succeeds

With --install-dir, the executable and possibly its directory are created before connecting to the SCM or checking whether the service already exists. If any later step fails, these artifacts are left without recorded ownership, and a repeated install targeting an existing stopped service can overwrite its registered binary before returning service satpulsed already exists. Check the service first and roll back the copy and directory on subsequent installation failures.

Useful? React with 👍 / 👎.

jclark added 2 commits August 11, 2026 12:22
The service support conflated registering the service with managing the
executable's placement. --uninstall deleted the --install-dir copy, which
broke when the installed copy itself ran the uninstall: Windows cannot
delete a running image, so the command failed after the service and Event
Log source were already gone, and the ownership metadata recorded in the
registry vanished with the service key. The copy also happened before the
SCM preflight, so a failed install left stray files behind.

Separate the concerns. --register and --unregister (renamed from
--install/--uninstall, since they manage only the SCM registration and,
unlike uninstall, carry no expectation of file removal) never delete
files, so the self-deletion problem disappears structurally and the
registry Parameters bookkeeping goes away. --install-dir becomes --copy:
an explicit convenience that copies the executable and registers the
copy. Unregister prints the registered executable's path when it is not
the running one, so the user knows what to remove by hand. Registration
now connects to the SCM and checks for an existing service before
copying, and removes the copy if a later step fails, so a failed
registration leaves nothing behind.
The service flags were redesigned during implementation: --register and
--unregister (renamed from --install/--uninstall) manage only the SCM
registration and never delete files, and --install-dir became --copy.
Update the archived plan to describe the implemented design, including
the reasoning for separating registration from file placement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run satpulsed as a Windows service

1 participant