6
6
7
7
import anyio
8
8
import anyio .lowlevel
9
+ import anyio .to_thread
10
+ import psutil
11
+ from anyio .abc import Process
9
12
from anyio .streams .memory import MemoryObjectReceiveStream , MemoryObjectSendStream
10
13
from anyio .streams .text import TextReceiveStream
11
14
from pydantic import BaseModel , Field
14
17
from mcp .shared .message import SessionMessage
15
18
16
19
from .win32 import (
20
+ FallbackProcess ,
17
21
create_windows_process ,
18
22
get_windows_executable_command ,
19
23
)
@@ -180,7 +184,7 @@ async def stdin_writer():
180
184
# MCP spec: stdio shutdown sequence
181
185
# 1. Close input stream to server
182
186
# 2. Wait for server to exit, or send SIGTERM if it doesn't exit in time
183
- # 3. Send SIGKILL if still not exited
187
+ # 3. Send SIGKILL if still not exited (forcibly kill on Windows)
184
188
if process .stdin :
185
189
try :
186
190
await process .stdin .aclose ()
@@ -193,18 +197,9 @@ async def stdin_writer():
193
197
with anyio .fail_after (2.0 ):
194
198
await process .wait ()
195
199
except TimeoutError :
196
- # Process didn't exit from stdin closure, escalate to SIGTERM
197
- try :
198
- process .terminate ()
199
- with anyio .fail_after (2.0 ):
200
- await process .wait ()
201
- except TimeoutError :
202
- # Process didn't respond to SIGTERM, force kill it
203
- process .kill ()
204
- await process .wait ()
205
- except ProcessLookupError :
206
- # Process already exited, which is fine
207
- pass
200
+ # Process didn't exit from stdin closure, use our termination function
201
+ # that handles child processes properly
202
+ await _terminate_process_with_children (process )
208
203
except ProcessLookupError :
209
204
# Process already exited, which is fine
210
205
pass
@@ -245,6 +240,70 @@ async def _create_platform_compatible_process(
245
240
if sys .platform == "win32" :
246
241
process = await create_windows_process (command , args , env , errlog , cwd )
247
242
else :
248
- process = await anyio .open_process ([command , * args ], env = env , stderr = errlog , cwd = cwd )
243
+ process = await anyio .open_process (
244
+ [command , * args ],
245
+ env = env ,
246
+ stderr = errlog ,
247
+ cwd = cwd ,
248
+ start_new_session = True ,
249
+ )
249
250
250
251
return process
252
+
253
+
254
+ async def _terminate_process_with_children (process : Process | FallbackProcess , timeout : float = 2.0 ) -> None :
255
+ """
256
+ Terminate a process and all its children using psutil.
257
+
258
+ This provides consistent behavior across platforms and properly
259
+ handles process trees without shell commands.
260
+
261
+ Platform behavior:
262
+ - On Unix: psutil.terminate() sends SIGTERM, allowing graceful shutdown
263
+ - On Windows: psutil.terminate() calls TerminateProcess() which is immediate
264
+ and doesn't allow cleanup handlers to run. This can cause ResourceWarnings
265
+ for subprocess.Popen objects that don't get to clean up.
266
+ """
267
+ pid = getattr (process , "pid" , None )
268
+ if pid is None :
269
+ popen = getattr (process , "popen" , None )
270
+ if popen :
271
+ pid = getattr (popen , "pid" , None )
272
+
273
+ if not pid :
274
+ # Process has no PID, cannot terminate
275
+ return
276
+
277
+ try :
278
+ parent = psutil .Process (pid )
279
+ children = parent .children (recursive = True )
280
+
281
+ # First, try graceful termination for all children
282
+ for child in children :
283
+ try :
284
+ child .terminate ()
285
+ except psutil .NoSuchProcess :
286
+ pass
287
+
288
+ # Then, also terminate the parent process
289
+ try :
290
+ parent .terminate ()
291
+ except psutil .NoSuchProcess :
292
+ return
293
+
294
+ # Wait for processes to exit gracefully, force kill any that remain
295
+ all_procs = children + [parent ]
296
+ _ , alive = await anyio .to_thread .run_sync (lambda : psutil .wait_procs (all_procs , timeout = timeout ))
297
+ for proc in alive :
298
+ try :
299
+ proc .kill ()
300
+ except psutil .NoSuchProcess :
301
+ pass
302
+
303
+ # Wait a bit more for force-killed processes
304
+ if alive :
305
+ await anyio .to_thread .run_sync (lambda : psutil .wait_procs (alive , timeout = 0.5 ))
306
+
307
+ except psutil .NoSuchProcess :
308
+ # Process already terminated
309
+ pass
0 commit comments