firebase / firebase/firebase-tools
Functions emulator orphans the Python runtime: functions-framework survives shutdown still bound to its port
- 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
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