|
1 | 1 | import logging
|
2 | 2 | import os
|
3 |
| -import signal |
4 | 3 | import sys
|
5 | 4 | from contextlib import asynccontextmanager
|
6 | 5 | from pathlib import Path
|
|
14 | 13 | from pydantic import BaseModel, Field
|
15 | 14 |
|
16 | 15 | import mcp.types as types
|
17 |
| -from mcp.shared.message import SessionMessage |
18 |
| - |
19 |
| -from .win32 import ( |
| 16 | +from mcp.os.posix.utilities import terminate_posix_process_tree |
| 17 | +from mcp.os.win32.utilities import ( |
20 | 18 | FallbackProcess,
|
21 | 19 | create_windows_process,
|
22 | 20 | get_windows_executable_command,
|
23 | 21 | terminate_windows_process_tree,
|
24 | 22 | )
|
| 23 | +from mcp.shared.message import SessionMessage |
25 | 24 |
|
26 |
| -logger = logging.getLogger("client.stdio") |
| 25 | +logger = logging.getLogger(__name__) |
27 | 26 |
|
28 | 27 | # Environment variables to inherit by default
|
29 | 28 | DEFAULT_INHERITED_ENV_VARS = (
|
@@ -247,47 +246,18 @@ async def _create_platform_compatible_process(
|
247 | 246 | return process
|
248 | 247 |
|
249 | 248 |
|
250 |
| -async def _terminate_process_tree(process: Process | FallbackProcess, timeout: float = 2.0) -> None: |
| 249 | +async def _terminate_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None: |
251 | 250 | """
|
252 | 251 | Terminate a process and all its children using platform-specific methods.
|
253 | 252 |
|
254 | 253 | Unix: Uses os.killpg() for atomic process group termination
|
255 | 254 | Windows: Uses Job Objects via pywin32 for reliable child process cleanup
|
| 255 | +
|
| 256 | + Args: |
| 257 | + process: The process to terminate |
| 258 | + timeout_seconds: Timeout in seconds before force killing (default: 2.0) |
256 | 259 | """
|
257 | 260 | if sys.platform == "win32":
|
258 |
| - await terminate_windows_process_tree(process, timeout) |
| 261 | + await terminate_windows_process_tree(process, timeout_seconds) |
259 | 262 | else:
|
260 |
| - pid = getattr(process, "pid", None) or getattr(getattr(process, "popen", None), "pid", None) |
261 |
| - if not pid: |
262 |
| - return |
263 |
| - |
264 |
| - try: |
265 |
| - pgid = os.getpgid(pid) |
266 |
| - os.killpg(pgid, signal.SIGTERM) |
267 |
| - |
268 |
| - with anyio.move_on_after(timeout): |
269 |
| - while True: |
270 |
| - try: |
271 |
| - # Check if process group still exists (signal 0 = check only) |
272 |
| - os.killpg(pgid, 0) |
273 |
| - await anyio.sleep(0.1) |
274 |
| - except ProcessLookupError: |
275 |
| - return |
276 |
| - |
277 |
| - try: |
278 |
| - os.killpg(pgid, signal.SIGKILL) |
279 |
| - except ProcessLookupError: |
280 |
| - pass |
281 |
| - |
282 |
| - except (ProcessLookupError, PermissionError, OSError) as e: |
283 |
| - logger.warning(f"Process group termination failed for PID {pid}: {e}, falling back to simple terminate") |
284 |
| - try: |
285 |
| - process.terminate() |
286 |
| - with anyio.fail_after(timeout): |
287 |
| - await process.wait() |
288 |
| - except Exception as term_error: |
289 |
| - logger.warning(f"Process termination failed for PID {pid}: {term_error}, attempting force kill") |
290 |
| - try: |
291 |
| - process.kill() |
292 |
| - except Exception as kill_error: |
293 |
| - logger.error(f"Failed to kill process {pid}: {kill_error}") |
| 263 | + await terminate_posix_process_tree(process, timeout_seconds) |
0 commit comments