Improve child process termination on POSIX & Windows (#1078)

Co-authored-by: jingx8885 <jingxu8885@qq.com>
Co-authored-by: Surya Prakash Susarla <susarla.surya.prakash.1998@gmail.com>
This commit is contained in:
Felix Weinberger 2025-07-08 15:04:06 +01:00 • committed by GitHub
commit e0336213e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,4 @@
import logging
import os
import sys
from contextlib import asynccontextmanager
@ -6,17 +7,22 @@ from typing import Literal, TextIO
import anyio
import anyio.lowlevel
from anyio.abc import Process
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from anyio.streams.text import TextReceiveStream
from pydantic import BaseModel, Field
import mcp.types as types
from mcp.shared.message import SessionMessage
from .win32 import (
from mcp.os.posix.utilities import terminate_posix_process_tree
from mcp.os.win32.utilities import (
FallbackProcess,
create_windows_process,
get_windows_executable_command,
terminate_windows_process_tree,
)
from mcp.shared.message import SessionMessage
logger = logging.getLogger(__name__)
# Environment variables to inherit by default
DEFAULT_INHERITED_ENV_VARS = (
@ -187,7 +193,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
await process.wait()
except TimeoutError:
# If process doesn't terminate in time, force kill it
process.kill()
await _terminate_process_tree(process)
except ProcessLookupError:
# Process already exited, which is fine
pass
@ -222,11 +228,38 @@ async def _create_platform_compatible_process(
):
"""
Creates a subprocess in a platform-compatible way.
Returns a process handle.
Unix: Creates process in a new session/process group for killpg support
Windows: Creates process in a Job Object for reliable child termination
"""
if sys.platform == "win32":
process = await create_windows_process(command, args, env, errlog, cwd)
else:
process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
process = await anyio.open_process(
[command, *args],
env=env,
stderr=errlog,
cwd=cwd,
start_new_session=True,
)
return process
async def _terminate_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None:
"""
Terminate a process and all its children using platform-specific methods.
Unix: Uses os.killpg() for atomic process group termination
Windows: Uses Job Objects via pywin32 for reliable child process cleanup
Args:
process: The process to terminate
timeout_seconds: Timeout in seconds before force killing (default: 2.0)
"""
if sys.platform == "win32":
await terminate_windows_process_tree(process, timeout_seconds)
else:
# FallbackProcess should only be used for Windows compatibility
assert isinstance(process, Process)
await terminate_posix_process_tree(process, timeout_seconds)