Skip to content

fix(cli): exit via process::exit to avoid tokio stdin-blocking-pool hang - #321

Merged
bug-ops merged 1 commit into
mainfrom
308-sigterm-stdin-hang
Aug 5, 2026
Merged

fix(cli): exit via process::exit to avoid tokio stdin-blocking-pool hang#321
bug-ops merged 1 commit into
mainfrom
308-sigterm-stdin-hang

Conversation

@bug-ops

@bug-ops bug-ops commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • mcpls did not exit on SIGTERM/SIGINT while a stdio MCP client kept its stdin write end open. rmcp::transport::stdio() is backed by tokio::io::stdin(), which internally parks an uncancellable spawn_blocking thread in a raw read() syscall; #[tokio::main]'s generated wrapper blocks in Runtime::drop waiting for that thread once main's body returns, even though all real shutdown work (LSP server teardown, log flush) had already completed. main now calls std::process::exit as its final step instead of returning normally. Scoped to crates/mcpls-cli/src/main.rs only; mcpls-core's serve/serve_with/shutdown/run_stdio are unaffected.
  • Also fixes await_lsp_init_handle's timeout branch, which called JoinHandle::abort() without a subsequent await: a SIGTERM arriving mid-spawn_batch could let process::exit run before the runtime dropped the aborted task's locals (including not-yet-registered LSP Child handles relying on kill_on_drop), orphaning those processes — the exact failure mode fix(transport): handle SIGINT/SIGTERM in stdio transport and gracefully shut down LSP servers #270 guards against. abort() is now followed by a bounded re-await.
  • Updated public doc examples (lib.rs, transport.rs, crates/mcpls-core/README.md) that previously taught the pre-fix hanging pattern.
  • Added an e2e regression test (test_e2e_sigterm_exits_promptly_while_client_stdin_open, #[cfg(unix)]) that spawns the real binary, completes an MCP handshake, sends SIGTERM, and asserts prompt exit with status 0.

Fixes #308. Filed #318 as a separate, non-blocking follow-up for a distinct pre-existing race surfaced during test hardening (SIGTERM arriving before run_stdio's signal handler registers gets OS default disposition).

Test plan

  • cargo +nightly fmt --all -- --check
  • cargo clippy --all-targets --all-features --workspace -- -D warnings
  • cargo nextest run --workspace --all-features --lib --bins (639/639)
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
  • New e2e regression test run explicitly (--run-ignored ignored-only), 8/8 consecutive passes
  • Adversarial critique (rust-critic) and code review (rust-code-reviewer) both completed, all findings addressed

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust Rust code changes testing Test-related changes mcpls-core mcpls-core crate changes mcpls-cli mcpls-cli crate changes labels Aug 5, 2026
@bug-ops
bug-ops force-pushed the 308-sigterm-stdin-hang branch from d18cbbd to df8d77d Compare August 5, 2026 01:38
@bug-ops
bug-ops enabled auto-merge (squash) August 5, 2026 01:39
mcpls previously did not exit on SIGTERM/SIGINT while a stdio MCP
client kept its stdin write end open. rmcp::transport::stdio() is
backed by tokio::io::stdin(), which internally parks an uncancellable
spawn_blocking thread in a raw read() syscall; #[tokio::main]'s
generated wrapper blocks in Runtime::drop waiting for that thread once
main's body returns, even though all real shutdown work (LSP server
teardown, log flush) had already completed by then.

main now calls std::process::exit as its final step instead of
returning normally, terminating immediately once run().await resolves
and bypassing the blocking-pool wait. Scoped to
crates/mcpls-cli/src/main.rs only; mcpls-core's serve/serve_with/
shutdown/run_stdio are unaffected and keep normal Result-returning
semantics for library embedders.

Also fixes await_lsp_init_handle's timeout branch, which called
JoinHandle::abort() without a subsequent await: on a SIGTERM arriving
mid-spawn_batch (before any server registers), process::exit could
run before the runtime dropped the aborted task's locals, including
not-yet-registered LSP Child handles relying on kill_on_drop,
orphaning those processes. abort() is now followed by a bounded
re-await so that drop happens before main exits.

Fixes #308
@bug-ops
bug-ops force-pushed the 308-sigterm-stdin-hang branch from df8d77d to 6a051b4 Compare August 5, 2026 01:43
@bug-ops
bug-ops merged commit 8c3e172 into main Aug 5, 2026
28 checks passed
@bug-ops
bug-ops deleted the 308-sigterm-stdin-hang branch August 5, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation mcpls-cli mcpls-cli crate changes mcpls-core mcpls-core crate changes rust Rust code changes testing Test-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcpls process does not exit on SIGTERM/SIGINT while a stdio client is still connected

1 participant