Skip to content

fix: kill Java emulators on unexpected CLI process exits#10224

Draft
kevmoo wants to merge 3 commits intofirebase:mainfrom
kevmoo:cleanup_emulators_on_crash
Draft

fix: kill Java emulators on unexpected CLI process exits#10224
kevmoo wants to merge 3 commits intofirebase:mainfrom
kevmoo:cleanup_emulators_on_crash

Conversation

@kevmoo
Copy link
Copy Markdown
Contributor

@kevmoo kevmoo commented Mar 30, 2026

Previously, if the firebase CLI crashed abruptly
(e.g., due to an unhandled exception or explicit fatal error bypass)
during an active Emulator session, the detached Java background processes
created for emulators like Database, Firestore, and PubSub were left orphaned.
This caused unintended resource leakage and frustrating port collisions on subsequent emulator launches.

This commit resolves the issue by introducing a native process.on("exit")
cleanup hook to downloadableEmulators.ts that explicitly issues a
SIGKILL to all recorded Java sub-processes strictly right upon CLI
termination. The graceful shutdown stop() handler has also been fortified
with a SIGKILL timeout fallback mechanism specifically designed to terminate
definitively hanging JVM emulator instances.

An integration test suite was also explicitly authored in
scripts/emulator-tests/downloadableEmulators.spec.ts which correctly
wraps CLI execution, spins up an isolated orchestrator locally, internally
aborts, and validates the OS system call returns ESRCH, guaranteeing the
associated Java PID dissolved.

  • Tested standard UI / Graceful SIGINT timeout logic.
  • Simulated Hard CLI process crashes, immediately reaping lingering JVM processes.

Previously, if the `firebase` CLI crashed abruptly
(e.g., due to an unhandled exception or explicit fatal error bypass)
during an active Emulator session, the detached Java background processes
created for emulators like Database, Firestore, and PubSub were left orphaned.
This caused unintended resource leakage and frustrating port collisions on subsequent emulator launches.

This commit resolves the issue by introducing a native `process.on("exit")`
cleanup hook to `downloadableEmulators.ts` that explicitly issues a
`SIGKILL` to all recorded Java sub-processes strictly right upon CLI
termination. The graceful shutdown `stop()` handler has also been fortified
with a `SIGKILL` timeout fallback mechanism specifically designed to terminate
definitively hanging JVM emulator instances.

An integration test suite was also explicitly authored in
`scripts/emulator-tests/downloadableEmulators.spec.ts` which correctly
wraps CLI execution, spins up an isolated orchestrator locally, internally
aborts, and validates the OS system call returns `ESRCH`, guaranteeing the
associated Java PID dissolved.

- Tested standard UI / Graceful `SIGINT` timeout logic.
- Simulated Hard CLI process crashes, immediately reaping lingering JVM processes.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to ensure that detached Java emulator processes are forcefully terminated when the parent CLI process exits abruptly. It adds a process.on('exit') hook to trigger a SIGKILL on the emulator instance and updates the stop function to send a SIGKILL if the initial termination attempt times out. A new integration test has been added to verify this behavior. The review feedback focuses on adhering to the repository's TypeScript style guide by removing the use of any in catch blocks in favor of more type-safe alternatives.

Comment thread src/emulator/downloadableEmulators.ts Outdated
Comment thread src/emulator/downloadableEmulators.ts Outdated
Comment thread scripts/emulator-tests/downloadableEmulators.spec.ts Outdated
@kevmoo
Copy link
Copy Markdown
Contributor Author

kevmoo commented Mar 30, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a safety mechanism to ensure detached Java emulators are terminated when the CLI process exits abruptly. Key changes include adding a process exit hook to kill emulator instances and a SIGKILL fallback if an emulator fails to stop within the timeout period. An integration test was also added to verify these process constraints. Review feedback recommends replacing the use of 'any' with a specific error type in the test and restoring the async/await pattern for fatal error handling to ensure consistency and avoid floating promises.

);
} catch (e) {
// ESRCH directly maps to 'No such process'. This proves our SIGKILL 'on exit' safety net works!
expect((e as any).code).to.equal("ESRCH");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using any as an escape hatch, as per the repository style guide. You can use a more specific type like NodeJS.ErrnoException to access the code property on the error object thrown by process.kill.

Suggested change
expect((e as any).code).to.equal("ESRCH");
expect((e as NodeJS.ErrnoException).code).to.equal("ESRCH");
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment on lines +530 to 537
emulator.instance.once("exit", (code, signal) => {
process.removeListener("exit", exitCleanup);
if (signal) {
utils.logWarning(`${description} has exited upon receiving signal: ${signal}`);
} else if (code && code !== 0 && code !== /* SIGINT */ 130) {
await _fatal(emulator.name, `${description} has exited with code: ${code}`);
void _fatal(emulator.name, `${description} has exited with code: ${code}`);
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The previous implementation used async/await for the _fatal call. Restoring this pattern ensures consistency with other _fatal calls in this file (e.g., lines 407, 413, 420) and avoids floating promises, even if _fatal eventually terminates the process.

Suggested change
emulator.instance.once("exit", (code, signal) => {
process.removeListener("exit", exitCleanup);
if (signal) {
utils.logWarning(`${description} has exited upon receiving signal: ${signal}`);
} else if (code && code !== 0 && code !== /* SIGINT */ 130) {
await _fatal(emulator.name, `${description} has exited with code: ${code}`);
void _fatal(emulator.name, `${description} has exited with code: ${code}`);
}
});
emulator.instance.once("exit", async (code, signal) => {
process.removeListener("exit", exitCleanup);
if (signal) {
utils.logWarning(`${description} has exited upon receiving signal: ${signal}`);
} else if (code && code !== 0 && code !== /* SIGINT */ 130) {
await _fatal(emulator.name, `${description} has exited with code: ${code}`);
}
});

@kevmoo kevmoo marked this pull request as draft April 10, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants