patrol_mcp: don't listen for SIGTERM on Windows#3036
Conversation
ProcessSignal.sigterm.watch() throws SignalException on Windows ("OS Error:
The request is not supported, errno = 50"), crashing the MCP server before
it can answer an initialize request. As a result, MCP clients (Claude Code,
Cursor, etc.) on Windows cannot use patrol_mcp at all — the 5 tools never
load.
Guard the sigterm registration with !Platform.isWindows. SIGINT keeps its
registration unconditionally (Ctrl-C continues to work on all platforms),
and stdio-transport shutdown via stdin EOF is unaffected, so graceful
shutdown paths are preserved everywhere. The sigterm subscription becomes
nullable so _cleanup() works without hitting a LateInitializationError on
Windows.
Fixes leancodepl#3035
There was a problem hiding this comment.
Code Review
This pull request prevents a SignalException on Windows by guarding the ProcessSignal.sigterm subscription, which is unsupported on that platform. The review feedback suggests maintaining the immutability of the _sigtermSubscription field by using a conditional initialization expression and declaring the variable as late final.
| if (!Platform.isWindows) { | ||
| _sigtermSubscription = ProcessSignal.sigterm.watch().listen( | ||
| _handleSignal, | ||
| ); | ||
| } | ||
| _sigintSubscription = ProcessSignal.sigint.watch().listen(_handleSignal); |
There was a problem hiding this comment.
For better consistency and to adhere to Effective Dart's recommendation of using final for fields that are only assigned once, you can use a conditional expression to initialize _sigtermSubscription. This keeps the initialization logic concise and ensures the field remains immutable after its initial assignment.
_sigtermSubscription = !Platform.isWindows
? ProcessSignal.sigterm.watch().listen(_handleSignal)
: null;
_sigintSubscription = ProcessSignal.sigint.watch().listen(_handleSignal);_registerProxyCleanupOnSignals wraps the sigterm subscription in a try/catch, but on Windows the SignalException surfaces as an unhandled async error (the subscription is returned synchronously but the signal registration fails later inside the stream). The error kills the process during `patrol develop` startup — the MCP `run` tool dies with "Connection closed" before the first test-build step. Guard with Platform.isWindows, matching the patrol_mcp/_ExitSignal fix in the sibling package. SIGINT handling and stdio-close shutdown are unaffected, so graceful cleanup still runs on all platforms. Part of leancodepl#3035.
Gemini review feedback on leancodepl#3036: - Use a ternary expression in the constructor so the field is assigned in a single expression rather than a branching if/block. - Keep _sigtermSubscription `late final` (with a nullable type) to match _sigintSubscription and Effective Dart's guidance that fields assigned exactly once be `final`. Behaviour is unchanged — on Windows the field is initialised to null, on POSIX it's initialised to the sigterm subscription. _cleanup() continues to use ?.cancel() which handles the null case.
pdenert
left a comment
There was a problem hiding this comment.
Looks good, please add changelog entries to both packages
Closes #3035.
Problem
patrol_mcp0.1.3 fails to start on Windows.ProcessSignal.sigterm.watch()is not listenable there — it throwsSignalException: Failed to listen for SIGTERM, osError: OS Error: The request is not supported, errno = 50. The exception is raised synchronously from_ExitSignal's constructor, before the MCP server can answer aninitializerequest, so MCP clients (Claude Code, Cursor, etc.) on Windows never see any tools.Reproduction (from Git Bash or PowerShell on Windows):
Fix
Guard the sigterm registration with
!Platform.isWindows. The sigterm subscription becomes nullable so_cleanup()works without aLateInitializationErroron Windows.mcp_dart's stdio transport already handles.kill PID,docker stop, etc. still trigger the SIGTERM path on macOS / Linux.Windows has no POSIX SIGTERM; process termination there goes through
TerminateProcess(uncatchable, like SIGKILL) or the SIGINT path we already listen on. So nothing functional is lost by skipping registration on Windows — thewatch()call was already not running there, it was just crashing the process instead.Verification on Windows
After the patch, an MCP
initialize+tools/listexchange now returns all five tools (run,quit,status,screenshot,native-tree):Stdin close triggers a clean exit via
[DEBUG][mcp_dart.server.stdio] Stdin closed..Environment
patrol_mcp0.1.3ProcessSignal.sigtermunavailability, not shell-specific.Diff
One file, 11 insertions / 3 deletions:
packages/patrol_mcp/bin/patrol_mcp.dart. Inline comment in the code explains the guard for future readers.