Fix Tentacle: add UseWindowsService() so SCM-launched start works on Windows
Summary
- Adds
Microsoft.Extensions.Hosting.WindowsServicesintegration toSquid.Tentacle.exeso SCM-launched start (sc start squid-tentacle) reaches RUNNING state - Extracts
Program.cs's command-dispatch body into sharedTentacleEntry.RunAsyncso console mode + SCM mode share identical pipeline logic - New
TentacleEntry.ShouldRunUnderScmdetection seam (16 unit-test cases) decides which mode to use - New
TentacleScmHostedServicewraps the pipeline underWindowsServiceLifetimeso 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:
- Process started — but
StartServiceCtrlDispatcherwas never called - SCM remained in
START_PENDING - 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. SameConsole.CancelKeyPress+ProcessExitcancellation 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-
runcommand (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-Windowslands in a separate follow-up PR after Phase 13 PR-3 (#271) merges, so the SCM test can extendTentacleWindowsRealBinaryIntegrationE2ETests'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 Testsworkflow (Squid Tentacle Tests step) green -
CI: Tentacle Windows E2Eworkflow 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.