From 38f7da2badbf2b87f7ad0f5ac36933d1274d2d9b Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Thu, 8 May 2025 12:05:46 +0800 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 + + From 6afa60e055d99184676dfa80ebfa10b266f98ff2 Mon Sep 17 00:00:00 2001 From: jingx8885 Date: Mon, 30 Jun 2025 21:03:13 +0800 Subject: [PATCH 05/11] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9df92e1d1..1e9dfde90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1; sys_platform != 'emscripten'", - "psutil>=5.9.0, + "psutil>=5.9.0", "jsonschema>=4.20.0", ] From 92dd8609d335a23b4a2e7bb7e828e4dcf85e5d00 Mon Sep 17 00:00:00 2001 From: jingx8885 Date: Mon, 30 Jun 2025 21:10:33 +0800 Subject: [PATCH 06/11] Update win32.py --- src/mcp/client/stdio/win32.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index db844121a..0977ddbeb 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -62,6 +62,7 @@ def __init__(self, popen_obj: subprocess.Popen[bytes]): self.stdin_raw = popen_obj.stdin # type: ignore[assignment] self.stdout_raw = popen_obj.stdout # type: ignore[assignment] self.stderr = popen_obj.stderr # type: ignore[assignment] + self.pid = popen_obj.pid self.stdin = FileWriteStream(cast(BinaryIO, self.stdin_raw)) if self.stdin_raw else None self.stdout = FileReadStream(cast(BinaryIO, self.stdout_raw)) if self.stdout_raw else None From 826c419b7ed7e677fe28dd8b1ad89061d7117c63 Mon Sep 17 00:00:00 2001 From: jingx8885 Date: Mon, 30 Jun 2025 21:17:47 +0800 Subject: [PATCH 07/11] Update win32.py --- src/mcp/client/stdio/win32.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 0977ddbeb..d5dce5dab 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -166,6 +166,10 @@ async def terminate_windows_process(process: Process | FallbackProcess): """ Terminate a process and subprocesses. """ + try: + parent = psutil.Process(process.pid) + except psutil.NoSuchProcess: + return parent = psutil.Process(process.pid) children = parent.children(recursive=True) await terminate_psutil_process(children) From fab17dcf1db5cd6f6d7827c0b83ee9daeffc6c8d Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Mon, 30 Jun 2025 21:25:33 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E7=A7=BB=E9=99=A4win32.py=E4=B8=AD?= =?UTF-8?q?=E7=9A=84anyio=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcp/client/stdio/win32.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index d5dce5dab..48a253ff7 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import BinaryIO, TextIO, cast -import anyio import psutil from anyio import to_thread from anyio.abc import Process From e98a00dc19b5e57fe9247d2791458278dd0405f6 Mon Sep 17 00:00:00 2001 From: jingx8885 Date: Mon, 30 Jun 2025 21:32:43 +0800 Subject: [PATCH 09/11] Update win32.py --- src/mcp/client/stdio/win32.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index 48a253ff7..a37b6db27 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -169,6 +169,8 @@ async def terminate_windows_process(process: Process | FallbackProcess): parent = psutil.Process(process.pid) except psutil.NoSuchProcess: return + except Exception: + pass # Optionally log the exception parent = psutil.Process(process.pid) children = parent.children(recursive=True) await terminate_psutil_process(children) From 4dd5922726eef92d701ead9c66f17cfaef211a9f Mon Sep 17 00:00:00 2001 From: jingxu8885 Date: Mon, 30 Jun 2025 21:37:28 +0800 Subject: [PATCH 10/11] reformat --- src/mcp/client/stdio/win32.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mcp/client/stdio/win32.py b/src/mcp/client/stdio/win32.py index a37b6db27..20447b631 100644 --- a/src/mcp/client/stdio/win32.py +++ b/src/mcp/client/stdio/win32.py @@ -161,6 +161,7 @@ async def create_windows_process( ) return FallbackProcess(popen_obj) + async def terminate_windows_process(process: Process | FallbackProcess): """ Terminate a process and subprocesses. @@ -175,7 +176,7 @@ async def terminate_windows_process(process: Process | FallbackProcess): children = parent.children(recursive=True) await terminate_psutil_process(children) await terminate_psutil_process([parent]) - + async def terminate_psutil_process(processes: list[psutil.Process]): """ @@ -205,6 +206,3 @@ async def terminate_psutil_process(processes: list[psutil.Process]): pass # Already gone except Exception: pass - - - From 126df34fbb1f9156fc75c1e169ea411306fe4128 Mon Sep 17 00:00:00 2001 From: jingx8885 Date: Mon, 30 Jun 2025 21:51:27 +0800 Subject: [PATCH 11/11] Update RELEASE.md --- RELEASE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 6555a1c2d..8c3410ece 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,3 +11,5 @@ Create a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the and the release title being the same. Then ask someone to review the release. The package version will be set automatically from the tag. + +