firebase / firebase/firebase-tools

Functions emulator orphans the Python runtime: functions-framework survives shutdown still bound to its port

Open
#11,069 0 comments 0 reactions 0 assignees View on GitHub
api: functions reproducible type: bug
Dominant language
TypeScript
Stars
4.5k
Forks
1.3k
Avg merge
1d 12h
Merged PRs (30d)
84

Description

### What happens

The Functions emulator starts the Python runtime through `runWithVirtualEnv`, which always spawns via a shell (`. venv/bin/activate && functions-framework`). Every shutdown path in `functionsRuntimeWorker.ts` calls `runtime.process.kill()`, which signals that shell and not the Python process underneath it. The shell dies, `functions-framework` survives, reparents to PID 1, and keeps its TCP port bound.

This is the same root cause as #10847, fixed for the deploy-time discovery server in #10893. `startPython` in `src/emulator/functionsEmulator.ts` isn't covered by that fix: it neither spawns `detached` nor calls `trackVirtualEnvChild`, so neither the process-group kill nor the CLI-exit cleanup reaches it.

### Reproduction

firebase-tools 15.30.0, macOS (Darwin 25.6.0), Python 3.14, firebase-functions 0.4.3.

1. A Python functions project with a venv and one `on_request` function.
2. `firebase emulators:start --only functions --project demo-repro`
3. Invoke it once so the runtime worker actually starts: `curl http://127.0.0.1:5001/demo-repro/us-central1/hello`
4. Stop the emulator with `kill -INT `. It shuts down cleanly, logging "Stopping" for every emulator.

While running, the tree is shell -> Python -> reloader (columns are pid, ppid, command):

```text
86175 85898 /bin/sh -c . ".../venv/bin/activate" && functions-framework
86178 86175 .../venv/bin/functions-framework
86179 86178 .../venv/bin/functions-framework
```

After the CLI has exited, the shell is gone but the Python process is not:

```console
$ ps -o pid,ppid -p 86178
PID PPID
86178 1 # reparented to init

$ lsof -nP -a -p 86178 -iTCP -sTCP:LISTEN
Python 86178 ... TCP 127.0.0.1:8674 (LISTEN)

$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8674/
200 # still serving
```

Sending SIGINT to the CLI's pid alone is deliberate here: it isolates the CLI's own cleanup from the terminal signalling the whole foreground process group, which is what masks this during ordinary Ctrl-C.

### Impact

Milder than #10847, because `startPython` picks its port through `portfinder` and so a stale orphan does not block the next run. Instead each emulator session that invokes a Python function leaks a Python process that keeps running and serving until it is killed by hand.

`modulesDir()` in the deploy path also goes through the shell wrapper, but it is a short-lived `python -c` that is awaited to exit and binds no port, so it does not leak in practice.

Contributor guide

Open the contributing guide

Research direction

Start with startPython in src/emulator/functionsEmulator.ts and the shutdown paths in functionsRuntimeWorker.ts, then compare the deploy-time discovery fix in #10893, including trackVirtualEnvChild. Reproduce the SIGINT case from the issue and verify that the Python functions-framework process no longer survives CLI exit or keeps its port bound.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, shell, typescript
Domain
cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.