Skip to content

patrol_mcp: don't listen for SIGTERM on Windows#3036

Open
rsnpettrov wants to merge 3 commits into
leancodepl:masterfrom
rsnpettrov:fix/patrol-mcp-windows-sigterm
Open

patrol_mcp: don't listen for SIGTERM on Windows#3036
rsnpettrov wants to merge 3 commits into
leancodepl:masterfrom
rsnpettrov:fix/patrol-mcp-windows-sigterm

Conversation

@rsnpettrov
Copy link
Copy Markdown

Closes #3035.

Problem

patrol_mcp 0.1.3 fails to start on Windows. ProcessSignal.sigterm.watch() is not listenable there — it throws SignalException: 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 an initialize request, so MCP clients (Claude Code, Cursor, etc.) on Windows never see any tools.

Reproduction (from Git Bash or PowerShell on Windows):

dart run patrol_mcp
[INFO][patrol_mcp] Running MCP server on stdio
[INFO][patrol_mcp] Server started
Unhandled exception:
SignalException: Failed to listen for SIGTERM, osError: OS Error: The request is not supported, errno = 50

Fix

Guard the sigterm registration with !Platform.isWindows. The sigterm subscription becomes nullable so _cleanup() works without a LateInitializationError on Windows.

  • SIGINT keeps its registration unconditionally — Ctrl-C / Ctrl-Break continue to work on every platform.
  • stdio-transport shutdown is unaffected — MCP clients terminate the server by closing stdin, which mcp_dart's stdio transport already handles.
  • POSIX behavior is unchangedkill 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 — the watch() call was already not running there, it was just crashing the process instead.

Verification on Windows

After the patch, an MCP initialize + tools/list exchange now returns all five tools (run, quit, status, screenshot, native-tree):

> {"jsonrpc":"2.0","id":1,"method":"initialize", ...}
< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05", ...}}
> {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
< {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"run", ...}, {"name":"quit", ...}, {"name":"status", ...}, {"name":"screenshot", ...}, {"name":"native-tree", ...}]}}

Stdin close triggers a clean exit via [DEBUG][mcp_dart.server.stdio] Stdin closed..

Environment

  • Windows 11 (10.0.26200.8246)
  • Dart SDK 3.11.1 (stable, windows_x64)
  • Flutter 3.41.4 (stable)
  • patrol_mcp 0.1.3
  • Verified in Git Bash; behavior on PowerShell is identical because the root cause is Dart's ProcessSignal.sigterm unavailability, 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.

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
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/patrol_mcp/bin/patrol_mcp.dart Outdated
Comment on lines 245 to 250
if (!Platform.isWindows) {
_sigtermSubscription = ProcessSignal.sigterm.watch().listen(
_handleSignal,
);
}
_sigintSubscription = ProcessSignal.sigint.watch().listen(_handleSignal);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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);

Comment thread packages/patrol_mcp/bin/patrol_mcp.dart Outdated
_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.
@github-actions github-actions Bot added the package: patrol_cli Related to the patrol_cli package label Apr 22, 2026
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.
Copy link
Copy Markdown
Collaborator

@pdenert pdenert left a comment

Choose a reason for hiding this comment

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

Looks good, please add changelog entries to both packages

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: patrol_cli Related to the patrol_cli package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

patrol_mcp 0.1.3 crashes on Windows: SignalException for SIGTERM

2 participants