Skip to content

Egress gateway leaves port/pid files after SIGINT to a background uv run process group #17

Open

Description

@MakerFlyFly

What's broken

On Linux, sending SIGINT to a Bash-backgrounded uv run ... egress-gateway process group can leave explicitly requested port/pid files behind, even though the gateway exits with status 0 and its workers and listener are released.

This report is limited to the supplied background-launch reproduction. Real terminal Ctrl+C tests passed; it is not a claim that ordinary interactive Ctrl+C always fails.

Which layer

tools/egress-gateway/src/egress_gateway/cli.py, at its integration with uv and proxy.py shutdown. The native proxy command reproduces the same result, so this is not specific to the gateway wrapper.

How to reproduce

At the time of testing, upstream main (27268f28a253978137e0cfa17bc19ef6e8897c28) still has the separate startup failure. These tests use that base plus the already-reviewed startup patch; the four resulting files match commit 44ca53aa6e9610af5ed9bb5d707a996dd481ba15. No signal-handling fix was applied.

Use that runnable source and install its locked environment:

git clone https://github.com/MakerFlyFly/magpie.git
cd magpie
git checkout --detach 44ca53aa6e9610af5ed9bb5d707a996dd481ba15
uv sync --frozen --project tools/egress-gateway

Save the following as repro.sh, then run bash repro.sh from the repository root. It makes a denied request to confirm that the proxy is serving before delivering SIGINT. The script was also checked with bash repro.sh module and bash repro.sh proxy.

Standalone Bash reproduction
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Run with: bash repro.sh [console|module|proxy], from the repository root.
set -u
project=tools/egress-gateway
state=$(mktemp -d)
case ${1:-console} in
  console) command=(egress-gateway) ;;
  module) command=(python -m egress_gateway.cli) ;;
  proxy) command=(proxy --hostname 127.0.0.1 --plugins egress_gateway.allowlist.EgressAllowlistPlugin) ;;
  *) exit 2 ;;
esac
setsid uv run --frozen --project "$project" "${command[@]}" \
  --port 0 --port-file "$state/port" --pid-file "$state/pid" \
  --log-level DEBUG >"$state/log" 2>&1 &
leader=$!
for _ in $(seq 1 120); do
  [ -s "$state/port" ] && [ -s "$state/pid" ] && break
  sleep 0.25
done
gateway=$(cat "$state/pid")
pgid=$(ps -o pgid= -p "$leader" | tr -d ' ')
[ "$pgid" = "$leader" ] || exit 1
python - "$state/port" <<'PY'
import http.client, pathlib, sys, time
port = int(pathlib.Path(sys.argv[1]).read_text())
deadline = time.monotonic() + 20
while True:
    connection = http.client.HTTPConnection("127.0.0.1", port, timeout=1)
    try:
        connection.request("GET", "http://example.invalid/readiness")
        response = connection.getresponse()
        if response.status == 403 and b"host not on allowlist" in response.read():
            break
    except (OSError, http.client.HTTPException):
        pass
    finally:
        connection.close()
    if time.monotonic() >= deadline:
        raise SystemExit("No successful proxy response")
    time.sleep(0.1)
PY
[ "$?" = 0 ] || { kill -TERM "$gateway"; wait "$leader"; exit 1; }
sleep 1.5
kill -INT -- "-$pgid"
wait "$leader"
rc=$?
printf 'exit=%s handler_calls=%s port_file=%s pid_file=%s\n' "$rc" \
  "$(grep -c "pid:$gateway .*Received signal 2" "$state/log")" \
  "$([ -e "$state/port" ] && echo left || echo removed)" \
  "$([ -e "$state/pid" ] && echo left || echo removed)"

Observed with each entrypoint:

exit=0 handler_calls=2 port_file=left pid_file=left

Expected vs actual

Expected: a normal shutdown removes the requested state files, as direct execution and single-process signal delivery do in the controls.

Actual: the affected runs report success while retaining both files. The successful-response background reproductions were 3/3 for each of the console entrypoint, module entrypoint, and native proxy command. Runs without prior proxy traffic also reproduced 3/3 for each.

Separate diagnostic runs found the first SIGINT in sleep_loop and the second inside Proxy.shutdown() while AcceptorPool.shutdown() waits in multiprocessing.Process.join(). The second handler raises SystemExit(0) before normal shutdown reaches file deletion. Those instrumented runs are separate from the unchanged-handler reproduction above.

Surface area (optional)

Launch and signal delivery Result
Bash background + setsid; uv console/module/native proxy; SIGINT to group Both files retained, 6/6 per entrypoint across the two traffic conditions
Same Bash launch; SIGINT only to uv or only to gateway Both files removed, 3/3 for each target
Bash background; direct gateway or direct native proxy; group SIGINT Both files removed, 3/3 per command
Python start_new_session launcher, non-TTY Both files removed, 84/84
Real PTY, Ctrl+C byte, foreground process group verified Both files removed, 36/36

All 150 matrix/control runs exited 0 and released workers and listeners. The exact reason the Bash-background and Python-launcher cases differ has not been established. The demonstrated impact is stale explicitly requested state files; no higher-impact process-management consequence has been verified. Default invocations do not request these files.

Environment (optional)

  • Linux containers on WSL2 kernel 6.6.87.2; Docker Engine 29.8.1.
  • CPython 3.12.12; uv 0.11.14; locked proxy.py 2.4.10.
  • Image: ghcr.io/astral-sh/uv@sha256:85d4cb1afa769a7338e095b927bee941cf5ec92266c7424b3f6c0f2748567248.
  • Linux-native Git checkout, preserving all 490 tracked symlinks and file modes. Default concurrency and a one-worker control were tested separately.

Prepared with assistance from Codex (GPT-6). This fork issue is for contributor review before any upstream submission.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfamily:toolssubstrate:sandboxTool substrate: agent isolation, egress control, settings audit

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions