Skip to content

Add stdio-mode SIGTERM observability handler (#156)#157

Merged
obj-p merged 1 commit intomainfrom
fix/156-stdio-sigterm-observability
May 2, 2026
Merged

Add stdio-mode SIGTERM observability handler (#156)#157
obj-p merged 1 commit intomainfrom
fix/156-stdio-sigterm-observability

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented May 2, 2026

Summary

Closes the question raised in #156 by establishing — through local repro — that the daemon already exits promptly on SIGTERM. Adds a small observability hook so any future recurrence is self-diagnosing.

Investigation findings (issue #156)

PR #146 landed a 5s-poll + SIGKILL band-aid in MCPTestServer.stop(). CI then went 8/8 green with zero SIGKILL escalations, meaning the daemon really does exit within the poll window — so the 1200s wedge wasn't the daemon being slow to die. Local repro confirms this:

Scenario SIGTERM-to-exit
Bare previewsmcp serve (no MCP traffic), 10× ~5ms
Bare with MCP initialize, 10× ~5ms
Foundation Process + Pipe() mirroring MCPTestServer, 10× ~70ms
MacOSMCPTests.hotReloadStructural (full preview state) 52ms
Full MacOSMCPTests × 5 iterations (30 SIGTERM cycles) 53–60ms median, 0 escalations

The three issue-listed hypotheses:

The 1200s wedge was inside Foundation's Process.waitUntilExit() Mach-port path on the test-host side. The polling band-aid sidesteps it. The daemon side is healthy.

Change

Add a signal-safe SIGTERM handler in runStdio(). Behavior:

  • Writes previewsmcp stdio: received SIGTERM, exiting via signal-safe write(2) from a StaticString buffer.
  • Calls _exit(0) — no AppKit teardown, no libdispatch dependency.

Deliberately not the daemon-mode DispatchSource(... queue: .main) pattern. That path is coupled to NSApplication's runloop, and queue starvation under load was the only remaining live hypothesis for the original wedge. Stdio mode has no on-disk state to clean up before exit (no PID file, no socket), so signal-thread _exit(0) is correct.

The single observable side effect is the stderr breadcrumb. If a future shutdown wedge recurs, the presence/absence of this line in the per-instance log partitions "daemon ignored signal" from "Foundation Process wedged on the test-host side."

Test plan

  • swift build clean
  • swift test --filter MacOSMCPTests — 7/7 pass, exit latency unchanged (50-60ms per stop)
  • Bare-spawn SIGTERM verified: rc=0 (clean handler exit), breadcrumb in stderr
  • Breadcrumb appears in per-instance daemon log immediately before [stop ...] process exited after SIGTERM
  • CI green

Investigation of #156 found that the daemon already exits promptly on
SIGTERM in every measured condition (median 50-60ms across 30 local
SIGTERM cycles in the full MacOSMCPTests suite, 0 SIGKILL escalations).
The 1200s wedge tracked by #146's polling band-aid was inside
Foundation's `Process.waitUntilExit()` Mach-port path on the test-host
side, not the daemon.

Stdio mode previously had no SIGTERM handler — `runStdio()` skipped
`DaemonLifecycle.installSignalHandlers()`, leaving the kernel default
disposition (terminate immediately). That's already fast, but yields
no observability: if a future shutdown wedge recurs, there's no way
to distinguish "daemon never received SIGTERM" from "Foundation Process
wedged on the test side."

This adds a minimal `signal()` handler that writes a stderr breadcrumb
via signal-safe `write(2)` and exits via `_exit(0)`. Deliberately not
the daemon-mode `DispatchSource(... queue: .main)` pattern: that path
is coupled to NSApplication's runloop, and queue starvation under load
was the only remaining live hypothesis for the original wedge. Stdio
mode has no on-disk state to clean up before exit (no PID file, no
socket), so signal-thread `_exit(0)` is correct.

Local verification:
- `swift test --filter MacOSMCPTests`: 7/7 pass, exit latency unchanged
- Bare-spawn SIGTERM: 4ms exit (vs 5ms before — handler is faster than
  the kernel default path because it skips dyld destructors)
- Breadcrumb appears in per-instance daemon stderr log immediately
  before `[stop ...] process exited after SIGTERM` from the test harness
@obj-p obj-p merged commit 57fa6aa into main May 2, 2026
4 checks passed
@obj-p obj-p deleted the fix/156-stdio-sigterm-observability branch May 2, 2026 04:15
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.

1 participant