From 422384e576ac2ac34d58db9473a7db888b5ce258 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:35:39 -0500 Subject: [PATCH] Fix --reload port conflict when using explicit port (#3070) * Fix --reload port conflict by killing entire process group * Gate start_new_session on Unix (no-op on Windows) --- src/fastmcp/cli/run.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 64fbf0d06..68ec876bb 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -3,6 +3,7 @@ import asyncio import contextlib import json +import os import re import signal import sys @@ -288,10 +289,34 @@ def _watch_filter(_change: Change, path: str) -> bool: async def _terminate_process(process: asyncio.subprocess.Process) -> None: - """Terminate a subprocess immediately.""" + """Terminate a subprocess and all its children. + + Sends SIGTERM to the process group first for graceful shutdown, + then falls back to SIGKILL if the process doesn't exit in time. + """ if process.returncode is not None: return - process.kill() + + pid = process.pid + + if sys.platform != "win32": + # Send SIGTERM to the entire process group for graceful shutdown + with contextlib.suppress(ProcessLookupError, OSError): + os.killpg(os.getpgid(pid), signal.SIGTERM) + + # Wait briefly for graceful exit + try: + await asyncio.wait_for(process.wait(), timeout=3.0) + return + except asyncio.TimeoutError: + pass + + # Force kill the entire process group + with contextlib.suppress(ProcessLookupError, OSError): + os.killpg(os.getpgid(pid), signal.SIGKILL) + else: + process.kill() + await process.wait() @@ -347,6 +372,8 @@ async def run_with_reload( stdin=None, stdout=None, stderr=None, + # Own process group so _terminate_process can kill the whole tree + start_new_session=sys.platform != "win32", ) # Watch for either: file changes OR process death