From 38f7da2badbf2b87f7ad0f5ac36933d1274d2d9b Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Thu, 8 May 2025 12:05:46 +0800 Subject: [PATCH 1/4] fix(win32): ensure proper termination of child processes - Refactor process termination logic to handle both parent and child processes - Increase timeout for graceful termination from 1s to 2s - Consolidate process termination functions to avoid zombie processes - Add proper type hints for process list handling This change ensures that all child processes are properly terminated when killing a parent process on Windows, preventing zombie processes from being left behind. --- pyproject.toml | 1 + src/mcp/client/stdio/win32.py | 39 ++++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b86fb377..e890f8f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1; sys_platform != 'emscripten'", + "psutil>=5.9.0" ] [project.optional-dependencies] diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 825a0477d..ad61e4138 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -6,8 +6,8 @@ import subprocess import sys from pathlib import Path -from typing import TextIO - +from typing import List, TextIO +import psutil import anyio from anyio.abc import Process @@ -87,8 +87,17 @@ async def create_windows_process( ) return process - async def terminate_windows_process(process: Process): + """ + Terminate a process and subprocesses. + """ + parent = psutil.Process(process.pid) + children = parent.children(recursive=True) + await terminate_psutil_process(children) + await terminate_psutil_process([parent]) + + +async def terminate_psutil_process(processes: List[psutil.Process]): """ Terminate a Windows process. @@ -100,10 +109,20 @@ async def terminate_windows_process(process: Process): Args: process: The process to terminate """ - try: - process.terminate() - with anyio.fail_after(2.0): - await process.wait() - except TimeoutError: - # Force kill if it doesn't terminate - process.kill() + for process in processes: + try: + process.terminate() # Send SIGTERM (or equivalent on Windows) + except psutil.NoSuchProcess: + pass + except Exception: + pass + # Allow some time for children to terminate gracefully + _, alive = psutil.wait_procs(processes, timeout=2.0) + for child in alive: + try: + child.kill() # Force kill if still alive + except psutil.NoSuchProcess: + pass # Already gone + except Exception: + pass + From 645ca4f1edd7be0fa1fe7f8e9ccdad5a5f02f819 Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Thu, 8 May 2025 12:36:01 +0800 Subject: [PATCH 2/4] update uv.lock --- uv.lock | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/uv.lock b/uv.lock index 88869fa50..cc53d8ad2 100644 --- a/uv.lock +++ b/uv.lock @@ -495,6 +495,7 @@ dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "psutil" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, @@ -538,6 +539,7 @@ requires-dist = [ { name = "anyio", specifier = ">=4.5" }, { name = "httpx", specifier = ">=0.27" }, { name = "httpx-sse", specifier = ">=0.4" }, + { name = "psutil", specifier = ">=5.9.0" }, { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, @@ -1064,6 +1066,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "psutil" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/b6/ea8a7728f096a597f0032564e8013b705aa992a0990becd773dcc4d7b4a7/psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25", size = 478322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/48/2c6f566d35a38fb9f882e51d75425a6f1d097cb946e05b6aff98d450a151/psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492", size = 238624 }, + { url = "https://files.pythonhosted.org/packages/11/46/e790221e8281af5163517a17a20c88b10a75a5642d9c5106a868f2879edd/psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3", size = 279343 }, + { url = "https://files.pythonhosted.org/packages/6f/8a/d1810472a4950a31df385eafbc9bd20cde971814ff6533021dc565bf14ae/psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2", size = 281400 }, + { url = "https://files.pythonhosted.org/packages/61/93/4251cfa58e5bbd7f92e1bfb965a0c41376cbcbc83c524a8b60d2678f0edd/psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d", size = 241383 }, + { url = "https://files.pythonhosted.org/packages/9f/c9/7fb339d6a04db3b4ab94671536d11e03b23c056d1604e50e564075a96cd8/psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b", size = 245540 }, +] + [[package]] name = "pycparser" version = "2.22" From ad2fcaf75ad0ef9dbe7142b4a4ecfbdc8415c5c7 Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Thu, 8 May 2025 12:39:53 +0800 Subject: [PATCH 3/4] format required --- src/mcp/client/stdio/win32.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index ad61e4138..aaad3543c 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path -from typing import List, TextIO +from typing import TextIO import psutil import anyio from anyio.abc import Process @@ -97,7 +97,7 @@ async def terminate_windows_process(process: Process): await terminate_psutil_process([parent]) -async def terminate_psutil_process(processes: List[psutil.Process]): +async def terminate_psutil_process(processes: list[psutil.Process]): """ Terminate a Windows process. From 5e096f2afd077a184b18c7540aa061379c6ee591 Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Thu, 8 May 2025 12:49:03 +0800 Subject: [PATCH 4/4] format win32.py --- src/mcp/client/stdio/win32.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index aaad3543c..b8378021d 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -7,8 +7,9 @@ import sys from pathlib import Path from typing import TextIO -import psutil + import anyio +import psutil from anyio.abc import Process @@ -126,3 +127,5 @@ async def terminate_psutil_process(processes: list[psutil.Process]): except Exception: pass + +