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:
parent
cf72565d9a
commit
e0336213e8
1 changed files with 552 additions and 15 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue