Skip to content

Fix Tentacle: add UseWindowsService() so SCM-launched start works on Windows

Placeholder ppxd requested to merge fix/use-windows-service-integration into main

Summary

  • Adds Microsoft.Extensions.Hosting.WindowsServices integration to Squid.Tentacle.exe so SCM-launched start (sc start squid-tentacle) reaches RUNNING state
  • Extracts Program.cs's command-dispatch body into shared TentacleEntry.RunAsync so console mode + SCM mode share identical pipeline logic
  • New TentacleEntry.ShouldRunUnderScm detection seam (16 unit-test cases) decides which mode to use
  • New TentacleScmHostedService wraps the pipeline under WindowsServiceLifetime so SCM's Stop signal drains in-flight work before shutdown

The production gap (surfaced during Phase 13 PR-3 scoping)

Pre-fix, when SCM launched the binary:

  1. Process started — but StartServiceCtrlDispatcher was never called
  2. SCM remained in START_PENDING
  3. After 30s: ERROR_SERVICE_REQUEST_TIMEOUT — service marked as failed

Operators following the documented service install + sc start workflow on Windows hit this immediately. Phase 13 PR-3 sidestepped with Process.Start to validate the polling code path; this PR delivers the production fix.

Post-fix SCM lifecycle

sc start squid-tentacle
  → WindowsServiceLifetime calls StartServiceCtrlDispatcher
  → SCM: START_PENDING → RUNNING
  → Tentacle starts polling, accepts deployments
  → ...
  → operator runs `sc stop squid-tentacle`
  → WindowsServiceLifetime cancels host CT
  → TentacleEntry.RunAsync sees CT cancellation
  → polling loop drains in-flight scripts (ShutdownDrainTimeout)
  → host disposes
  → SCM: STOP_PENDING → STOPPED

Architecture

Component Concern
TentacleEntry.RunAsync(args, ct) Shared command-dispatch pipeline (console mode + SCM mode use identical logic)
TentacleEntry.ShouldRunUnderScm(args, isWindows, isLaunchedBySCM) Pure detection seam — true only when (Windows + launched-by-SCM + command-is-run). Predicate as Func<bool> so tests run on any OS
TentacleScmHostedService BackgroundService running TentacleEntry.RunAsync under WindowsServiceLifetime's CT
Program.cs SCM branch Builds Host with AddWindowsService() + TentacleScmHostedService when SCM-launched; falls through to existing console flow otherwise

Behavior preservation

  • Console mode (CLI invocation, register, service install, show-thumbprint, etc.): zero change. Same Console.CancelKeyPress + ProcessExit cancellation source, same exit codes, same Serilog output (still routed to stderr per PR #264).
  • systemd-launched mode (Linux): zero change. WindowsServiceHelpers.IsWindowsService() returns false on Linux → falls through to console flow (same as today).
  • SCM-launched mode (Windows, NEW): takes the SCM lifetime path. Honors Stop signal cleanly.

Coverage

  • Unit tests (TentacleEntryTests.cs): 16 cases covering
    • Non-Windows short-circuit (predicate must NOT be invoked)
    • Console mode short-circuit on Windows
    • Every non-run command (register, show-thumbprint, show-config, list-instances, create-instance, delete-instance, new-certificate, version, service)
    • Happy path (Windows + SCM + run)
    • Multi-instance args (run --instance NAME)
    • Null validation (args + predicate)
  • Cross-platform: unit tests run identically on macOS / Linux / Windows
  • E2E variant deferred: SCM-launched variant of R1h-Windows lands in a separate follow-up PR after Phase 13 PR-3 (#271) merges, so the SCM test can extend TentacleWindowsRealBinaryIntegrationE2ETests's existing surface

Test plan

  • dotnet build src/Squid.Tentacle/Squid.Tentacle.csproj — 0 errors
  • dotnet test tests/Squid.Tentacle.Tests --filter TentacleEntryTests — 16/16 passing locally
  • CI: standard Tests workflow (Squid Tentacle Tests step) green
  • CI: Tentacle Windows E2E workflow green — phase-13 R1h-Windows still passes (Process.Start path unchanged)

Why this didn't surface earlier

The production binary's pre-fix behavior was: when launched directly (./Squid.Tentacle.exe run), it worked correctly because there's no SCM expectation. The bug only manifested when SCM (via service install + sc start) was the launcher. Existing Windows tests use a separate test-only ServiceBase-derived exe (SquidUpgradeE2ETestService.exe) for upgrade tests — they never spun up the real Squid.Tentacle.exe under SCM. Phase 13 PR-3 was the first attempt to do that, and it surfaced this gap.

🤖 Generated with Claude Code

Merge request reports

Loading