Skip to content

Commit a32ddda

Browse files
Add debug logging to diagnose Windows hanging issue
1 parent ab8eec9 commit a32ddda

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

debug_windows_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Debug script to understand Windows hanging issue"""
2+
import asyncio
3+
import sys
4+
import time
5+
import textwrap
6+
import logging
7+
from mcp.client.stdio import StdioServerParameters, stdio_client
8+
import anyio
9+
10+
# Enable debug logging
11+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(message)s')
12+
logger = logging.getLogger("mcp.stdio_debug")
13+
14+
async def test_with_debug():
15+
print("Starting debug test...")
16+
17+
long_running_script = textwrap.dedent(
18+
"""
19+
import time
20+
import sys
21+
22+
# Simulate a long-running process
23+
for i in range(100):
24+
time.sleep(0.1)
25+
# Flush to ensure output is visible
26+
sys.stdout.flush()
27+
sys.stderr.flush()
28+
"""
29+
)
30+
31+
server_params = StdioServerParameters(
32+
command=sys.executable,
33+
args=["-c", long_running_script],
34+
)
35+
36+
start_time = time.time()
37+
print(f"Test started at {start_time}")
38+
39+
try:
40+
with anyio.move_on_after(10.0) as cancel_scope:
41+
print("Entering stdio_client context...")
42+
async with stdio_client(server_params) as (read_stream, write_stream):
43+
print("Inside stdio_client context, immediately exiting...")
44+
pass
45+
46+
print("Exited stdio_client context")
47+
end_time = time.time()
48+
elapsed = end_time - start_time
49+
print(f"Cleanup took {elapsed:.2f} seconds")
50+
51+
if cancel_scope.cancelled_caught:
52+
print("ERROR: Timed out after 10 seconds!")
53+
sys.exit(1)
54+
else:
55+
print("SUCCESS: Cleanup completed without timeout")
56+
57+
except Exception as e:
58+
print(f"Exception occurred: {type(e).__name__}: {e}")
59+
import traceback
60+
traceback.print_exc()
61+
sys.exit(1)
62+
63+
if __name__ == "__main__":
64+
anyio.run(test_with_debug)

src/mcp/client/stdio/__init__.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,29 +185,47 @@ async def stdin_writer():
185185
# 1. Close input stream to server
186186
# 2. Wait for server to exit, or send SIGTERM if it doesn't exit in time
187187
# 3. Send SIGKILL if still not exited
188+
import logging
189+
logger = logging.getLogger("mcp.stdio_debug")
190+
logger.info("Starting cleanup sequence...")
191+
188192
if process.stdin:
189193
try:
194+
logger.info("Closing stdin...")
190195
await process.stdin.aclose()
191-
except Exception:
196+
logger.info("stdin closed")
197+
except Exception as e:
192198
# stdin might already be closed, which is fine
199+
logger.info(f"stdin close exception (ignored): {e}")
193200
pass
194201

195202
try:
196203
# Give the process time to exit gracefully after stdin closes
204+
logger.info("Waiting for process to exit after stdin closure...")
197205
with anyio.fail_after(2.0):
198206
await process.wait()
207+
logger.info("Process exited gracefully")
199208
except TimeoutError:
200209
# Process didn't exit from stdin closure, use our termination function
201210
# that handles child processes properly
211+
logger.info("Process didn't exit from stdin closure, terminating...")
202212
await _terminate_process_with_children(process)
213+
logger.info("Process terminated")
203214
except ProcessLookupError:
204215
# Process already exited, which is fine
216+
logger.info("Process already exited")
205217
pass
206218

219+
logger.info("Closing streams...")
207220
await read_stream.aclose()
221+
logger.info("read_stream closed")
208222
await write_stream.aclose()
223+
logger.info("write_stream closed")
209224
await read_stream_writer.aclose()
225+
logger.info("read_stream_writer closed")
210226
await write_stream_reader.aclose()
227+
logger.info("write_stream_reader closed")
228+
logger.info("Cleanup complete")
211229

212230

213231
def _get_executable_command(command: str) -> str:
@@ -265,6 +283,9 @@ async def _terminate_process_with_children(process: Process | FallbackProcess, t
265283
process: The process to terminate
266284
timeout: Time to wait for graceful termination before force killing
267285
"""
286+
import logging
287+
logger = logging.getLogger("mcp.stdio_debug")
288+
logger.info(f"_terminate_process_with_children called for process on {sys.platform}")
268289
if sys.platform != "win32":
269290
# POSIX: Kill entire process group to avoid orphaning children
270291
pid = getattr(process, "pid", None)
@@ -302,17 +323,23 @@ async def _terminate_process_with_children(process: Process | FallbackProcess, t
302323

303324
# Try graceful termination first
304325
try:
326+
logger.info(f"Calling terminate() on process with PID {pid}")
305327
process.terminate()
328+
logger.info("Waiting for process to exit after terminate()...")
306329
with anyio.fail_after(timeout):
307330
await process.wait()
331+
logger.info("Process exited after terminate()")
308332
except TimeoutError:
309333
# Force kill using taskkill for tree termination
310-
await anyio.to_thread.run_sync(
334+
logger.info(f"Process didn't exit after terminate(), force killing with taskkill")
335+
result = await anyio.to_thread.run_sync(
311336
subprocess.run,
312337
["taskkill", "/F", "/T", "/PID", str(pid)],
313338
capture_output=True,
314339
shell=False,
315340
check=False,
316341
)
342+
logger.info(f"taskkill result: returncode={result.returncode}, stdout={result.stdout}, stderr={result.stderr}")
317343
except ProcessLookupError:
344+
logger.info("Process already exited (ProcessLookupError)")
318345
pass

0 commit comments

Comments
 (0)