|
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