From bb8d782e42748e8d9f4e722c23667abbce7945f2 Mon Sep 17 00:00:00 2001 From: anti Date: Fri, 17 Apr 2026 15:32:08 -0400 Subject: [PATCH] fix(cli): kill uvicorn worker tree on Ctrl+C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With --workers > 1, SIGINT from the terminal raced uvicorn's supervisor: some workers got signaled directly, the supervisor respawned them, and the result behaved like a forkbomb. Start uvicorn in its own session and signal the whole process group (SIGTERM → 10s grace → SIGKILL) when we catch KeyboardInterrupt. --- decnet/cli.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/decnet/cli.py b/decnet/cli.py index 1e572fa..7226538 100644 --- a/decnet/cli.py +++ b/decnet/cli.py @@ -90,6 +90,7 @@ def api( import subprocess # nosec B404 import sys import os + import signal if daemon: log.info("API daemonizing host=%s port=%d workers=%d", host, port, workers) @@ -101,10 +102,23 @@ def api( _env["DECNET_INGEST_LOG_FILE"] = str(log_file) _cmd = [sys.executable, "-m", "uvicorn", "decnet.web.api:app", "--host", host, "--port", str(port), "--workers", str(workers)] + # Put uvicorn (and its worker children) in their own process group so we + # can signal the whole tree on Ctrl+C. Without this, only the supervisor + # receives SIGINT from the terminal and worker children may survive and + # be respawned — the "forkbomb" ANTI hit during testing. + proc = subprocess.Popen(_cmd, env=_env, start_new_session=True) # nosec B603 B404 try: - subprocess.run(_cmd, env=_env) # nosec B603 B404 + proc.wait() except KeyboardInterrupt: - pass + try: + os.killpg(proc.pid, signal.SIGTERM) + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + os.killpg(proc.pid, signal.SIGKILL) + proc.wait() + except ProcessLookupError: + pass except (FileNotFoundError, subprocess.SubprocessError): console.print("[red]Failed to start API. Ensure 'uvicorn' is installed in the current environment.[/]")