Skip to content

Commit 37d6224

Browse files
Fix FallbackProcess.wait() to use polling instead of blocking
- Replace blocking wait with polling loop in FallbackProcess.wait() - This allows anyio timeouts to work properly on Windows - Update test_stdio_client_universal_cleanup to use consistent Python script - Adjust timeout thresholds to account for Windows process termination overhead The polling approach prevents the wait() call from blocking indefinitely when used with anyio's timeout mechanisms, ensuring that cleanup operations can be properly interrupted if they take too long. Reported-by: Felix Weinberger 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d3d3448 commit 37d6224

File tree

2 files changed

+33
-22
lines changed

2 files changed

+33
-22
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import BinaryIO, TextIO, cast
1010

11-
from anyio import to_thread
11+
import anyio
1212
from anyio.streams.file import FileReadStream, FileWriteStream
1313

1414

@@ -91,7 +91,11 @@ async def __aexit__(
9191

9292
async def wait(self):
9393
"""Async wait for process completion."""
94-
return await to_thread.run_sync(self.popen.wait)
94+
# Poll the process status instead of blocking wait
95+
# This allows anyio timeouts to work properly
96+
while self.popen.poll() is None:
97+
await anyio.sleep(0.1)
98+
return self.popen.returncode
9599

96100
def terminate(self):
97101
"""Terminate the subprocess immediately."""

tests/client/test_stdio.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -105,42 +105,49 @@ async def test_stdio_client_universal_cleanup():
105105
even when connected to processes that exit slowly.
106106
"""
107107

108-
# Use a simple sleep command that's available on all platforms
109-
# This simulates a process that takes time to terminate
110-
if sys.platform == "win32":
111-
# Windows: use ping with timeout to simulate a running process
112-
server_params = StdioServerParameters(
113-
command="ping",
114-
args=["127.0.0.1", "-n", "10"], # Ping 10 times, takes ~10 seconds
115-
)
116-
else:
117-
# Unix: use sleep command
118-
server_params = StdioServerParameters(
119-
command="sleep",
120-
args=["10"], # Sleep for 10 seconds
121-
)
108+
# Use a Python script that simulates a long-running process
109+
# This ensures consistent behavior across platforms
110+
long_running_script = textwrap.dedent(
111+
"""
112+
import time
113+
import sys
114+
115+
# Simulate a long-running process
116+
for i in range(100):
117+
time.sleep(0.1)
118+
# Flush to ensure output is visible
119+
sys.stdout.flush()
120+
sys.stderr.flush()
121+
"""
122+
)
123+
124+
server_params = StdioServerParameters(
125+
command="python",
126+
args=["-c", long_running_script],
127+
)
122128

123129
start_time = time.time()
124130

125-
# Use move_on_after which is more reliable for cleanup scenarios
126-
with anyio.move_on_after(6.0) as cancel_scope:
131+
# Increased timeout to account for Windows process termination overhead
132+
with anyio.move_on_after(8.0) as cancel_scope:
127133
async with stdio_client(server_params) as (read_stream, write_stream):
128134
# Immediately exit - this triggers cleanup while process is still running
129135
pass
130136

131137
end_time = time.time()
132138
elapsed = end_time - start_time
133139

134-
# Key assertion: Should complete quickly due to timeout mechanism
135-
assert elapsed < 5.0, (
136-
f"stdio_client cleanup took {elapsed:.1f} seconds, expected < 5.0 seconds. "
140+
# Key assertion: Should complete within reasonable time due to timeout mechanism
141+
# On Windows: 2s (stdin wait) + 2s (terminate wait) + overhead = ~5s expected
142+
assert elapsed < 6.0, (
143+
f"stdio_client cleanup took {elapsed:.1f} seconds, expected < 6.0 seconds. "
137144
f"This suggests the timeout mechanism may not be working properly."
138145
)
139146

140147
# Check if we timed out
141148
if cancel_scope.cancelled_caught:
142149
pytest.fail(
143-
"stdio_client cleanup timed out after 6.0 seconds. "
150+
"stdio_client cleanup timed out after 8.0 seconds. "
144151
"This indicates the cleanup mechanism is hanging and needs fixing."
145152
)
146153

0 commit comments

Comments
 (0)