Skip to content

fix: forward signals from parent to child process to prevent orphans - #29427

Closed
dylanyunlon wants to merge 1 commit into
google-gemini:mainfrom
dylanyunlon:fix/signal-forwarding-25590
Closed

dylanyunlon wants to merge 1 commit into
google-gemini:mainfrom
dylanyunlon:fix/signal-forwarding-25590

Conversation

@dylanyunlon

Copy link
Copy Markdown

Summary

Fixes #25590

When the parent process (bootstrap in index.ts or relaunch wrapper in relaunch.ts) receives a termination signal (SIGTERM, SIGHUP, etc.), the child process was silently orphaned - reparented to PID 1 and left running indefinitely.

Root cause

Two spawn paths, same bug:

Path What happened
index.ts (lightweight parent) Installed empty signal handlers () => {} that swallowed SIGTERM/SIGHUP/SIGINT without forwarding to child
relaunch.ts (relaunchAppInChildProcess) No signal handlers at all, default disposition killed parent while child kept running

Fix

New: SignalForwarder class (@google/gemini-cli-core)

  • Installs per-signal handlers on process that forward to child.kill(sig)
  • Escalates to SIGKILL after configurable grace period (default 5s) for fatal signals
  • Idempotent install()/remove() prevents listener leaks across relaunch iterations
  • try/catch guards the race where signal arrives just after child exits (ESRCH)

Modified: relaunch.ts

  • Integrates SignalForwarder from core
  • Removes forwarders on both close and error events

Modified: index.ts

  • Replaces empty signal handlers with inline forwarding logic
  • Kept inline (not importing core) to preserve ~1.5s startup savings
  • Same escalation-to-SIGKILL behavior as the SignalForwarder class

Supporting changes

  • processUtils.ts: added isChildProcess() helper
  • cleanup.ts: documented interaction between child cleanup handlers and parent signal forwarding
  • test-rig.ts: set GEMINI_CLI_NO_RELAUNCH=true in test harness clean env
  • build_package.js: added build verification for the new module

AST call chain coverage

index.ts -> spawn() -> signalHandlers.set() -> child.kill()
relaunch.ts -> SignalForwarder.install() -> process.on() -> forwardSignal() -> child.kill()
                                                         -> escalateToKill() -> child.kill('SIGKILL')
cleanup.ts -> setupSignalHandlers() -> gracefulShutdown()
processUtils.ts -> isChildProcess() -> env check

Test coverage

Suite Tests What it covers
signalForwarding.test.ts 31 SignalForwarder class: install, remove, forwarding, escalation, idempotency, edge cases
relaunch.test.ts +7 new Signal forwarding in relaunch: SIGTERM, SIGHUP, SIGUSR1/2, error paths, listener leak prevention
signalForwarding.integration.test.ts 3 Real process spawning with OS-level signal delivery (SIGTERM, SIGHUP, SIGUSR1)
processUtils.test.ts +3 new isChildProcess()
cleanup.test.ts +2 new Signal handler coexistence with forwarding

Total: 46 new tests, all passing

Reproduction

# Before fix: child becomes orphan
node packages/cli/dist/index.js --prompt "hello" &
PID=$!; sleep 1
kill -TERM $PID  # parent dies, child stays alive (PPID -> 1)

# After fix: child receives forwarded SIGTERM and exits cleanly

Stats

  • 13 files changed, 1245 insertions, 6 deletions

When the parent process (bootstrap or relaunch wrapper) receives a
termination signal such as SIGTERM or SIGHUP, the child process is now
notified instead of being silently orphaned.

Root cause: both index.ts (lightweight parent) and relaunch.ts
(relaunchAppInChildProcess) spawned a child with stdio:'inherit' but
never proxied signals. The parent either swallowed them with empty
handlers (index.ts) or let the default disposition kill it while the
child kept running (relaunch.ts).

Fix:
- Add SignalForwarder class in @google/gemini-cli-core that installs
  precise per-signal handlers, forwards them to the child via
  child.kill(), and escalates to SIGKILL after a configurable grace
  period (default 5s) for fatal signals (SIGTERM, SIGHUP, SIGQUIT).
- Integrate into relaunch.ts via the shared SignalForwarder class.
- Integrate into index.ts with inline forwarding (avoids importing
  the full core package to keep startup fast).
- Clean up handlers on child 'close' and 'error' events to prevent
  listener leaks across relaunch iterations.
- Add isChildProcess() helper to processUtils.ts.
- Update cleanup.ts JSDoc to document interaction with forwarding.
- Add GEMINI_CLI_NO_RELAUNCH to test-rig.ts clean env.
- Add build verification in build_package.js for the new module.

Tests:
- 31 unit tests for SignalForwarder class (install, remove, forwarding,
  escalation, idempotency, edge cases).
- 7 unit tests for signal forwarding in relaunch.test.ts (SIGTERM,
  SIGHUP, SIGUSR1/2, error handling, listener leak prevention).
- 3 integration tests spawning real processes verifying OS-level signal
  delivery (SIGTERM, SIGHUP, SIGUSR1).
- 3 unit tests for isChildProcess().
- 2 unit tests for cleanup signal handler coexistence.

Fixes google-gemini#25590
@dylanyunlon
dylanyunlon requested a review from a team as a code owner September 20, 2026 12:51
@github-actions github-actions Bot added the size/xl An extra large PR label Sep 20, 2026
@github-actions

Copy link
Copy Markdown

You already have 7 pull requests open. Please work on getting existing PRs merged before opening more.

@github-actions

Copy link
Copy Markdown

📊 PR Size: size/XL

  • Lines changed: 1251
  • Additions: +1245
  • Deletions: -6
  • Files changed: 13

@github-actions github-actions Bot closed this Sep 20, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where child processes become orphaned when the parent process receives termination signals. By implementing a robust signal forwarding mechanism, the parent now correctly proxies signals to the child and enforces a grace period before escalating to SIGKILL. This ensures that child processes are properly cleaned up, improving system stability and resource management.

Highlights

  • Signal Forwarding Implementation: Introduced a SignalForwarder class to proxy termination signals from parent to child processes, preventing orphans.
  • Escalation Logic: Added automatic escalation to SIGKILL after a 5-second grace period for fatal signals to ensure clean termination.
  • Performance Optimization: Implemented inline signal forwarding in index.ts to avoid importing core modules, preserving startup performance.
  • Testing and Verification: Added 46 new tests, including integration tests, to verify signal delivery, listener cleanup, and orphan prevention.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

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

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.

Code Review

This pull request implements signal forwarding from the parent bootstrap process to the spawned child process to prevent orphaned child processes when the parent is terminated (addressing issue #25590). It introduces a SignalForwarder utility class in packages/core that handles forwarding of standard termination signals (SIGTERM, SIGHUP, SIGINT, SIGQUIT, SIGUSR1, SIGUSR2) and escalates to SIGKILL after a grace period for fatal signals. The parent CLI process utilizes this forwarder, and appropriate cleanup mechanisms are added to prevent listener leaks. Comprehensive unit, integration, and end-to-end tests are added to verify the behavior. I have no additional feedback to provide as the implementation is robust and well-tested.

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

Labels

size/xl An extra large PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: relaunchAppInChildProcess does not forward signals to child, orphaning it when parent is killed

1 participant