Skip to content

fix(cli): forward termination signals to relaunched child process - #25605

Closed
Sway-Chan wants to merge 5 commits into
google-gemini:mainfrom
Sway-Chan:fix/relaunch-signal-forward
Closed

fix(cli): forward termination signals to relaunched child process#25605
Sway-Chan wants to merge 5 commits into
google-gemini:mainfrom
Sway-Chan:fix/relaunch-signal-forward

Conversation

@Sway-Chan

Copy link
Copy Markdown

Summary

relaunchAppInChildProcess spawns a full-memory child via node:child_process.spawn, but the bootstrap parent did not install signal handlers to forward termination signals to the child. When the parent receives SIGTERM/SIGHUP from a supervising process (e.g. an ACP client, systemd, a container runtime), the bootstrap exits but the child is reparented to PID 1 / the user's systemd --user manager and keeps running, holding the OAuth session and allocated heap indefinitely.

This PR installs forwarders for the standard termination signals before awaiting the child, and removes them on close/error to avoid listener leaks across relaunch iterations.

Closes #25590

Reproduction (pre-patch)

gemini -m gemini-3.1-pro-preview -y --acp
# Separate terminal:
ps -eo pid,ppid,pgid,cmd | grep gemini   # observe bootstrap + child
kill -TERM <bootstrap-pid>
ps -eo pid,ppid,pgid,cmd | grep gemini   # child survives, PPID=1

Interactive Ctrl+C does not surface the bug because SIGINT is delivered to the foreground process group by the controlling tty. Only programmatic kill(pid, signal) against the parent exposes the leak — which is the normal path for any supervisor.

Root cause

packages/cli/src/utils/relaunch.ts, function relaunchAppInChildProcess:

  • The runner closure spawns the child and awaits close, but never calls process.on('SIGTERM'/'SIGHUP'/...) to proxy signals to child.kill(sig).
  • The parent therefore dies on its default signal disposition while the child keeps running.

Fix

Install a Map<NodeJS.Signals, handler> of forwarders for SIGTERM, SIGHUP, SIGINT, SIGQUIT, SIGUSR1, SIGUSR2 immediately after spawn. Each handler calls child.kill(sig) inside try/catch to tolerate the race where the child has already exited. The forwarders are removed on both child.on('close') and child.on('error') so that the per-iteration listener count stays bounded (otherwise Node logs a MaxListenersExceeded warning after ~10 relaunches).

Design notes:

  • Using a Map<signal, handler> rather than removeAllListeners keeps cleanup precise and avoids disturbing any other listeners the process may have installed.
  • detached: true is intentionally not introduced — it would change PGID/foreground-tty semantics and could break interactive Ctrl+C.
  • stdio: 'inherit' is unchanged; signal forwarding is orthogonal to stdio wiring.

Tests

Added two tests in packages/cli/src/utils/relaunch.test.ts:

  1. should forward termination signals to the child and clean up listeners on close — verifies:
    • After spawn, each of the six forwarded signals has exactly one additional listener on process.
    • Emitting SIGTERM on the parent triggers child.kill('SIGTERM').
    • After the child closes, all signal listener counts return to their baselines.
  2. should clean up signal listeners on child process error — verifies that if the child emits error, listeners are still removed (no leak on failure path).

All tests pass locally (vitest run packages/cli/src/utils/relaunch.test.ts: 10/10).

Downstream workaround (for users on older versions)

Setting GEMINI_CLI_NO_RELAUNCH=true skips the relaunch, which also avoids the bug — but it disables the --max-old-space-size tuning that the relaunch is designed to apply. This PR fixes the underlying issue so both features work together.

Compatibility

  • No public API changes.
  • No change to interactive behavior (Ctrl+C still routed via tty).
  • No change to stdio wiring or IPC messaging.

@Sway-Chan
Sway-Chan requested a review from a team as a code owner April 18, 2026 01:08
@google-cla

google-cla Bot commented Apr 18, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@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 spawned during application relaunch were not correctly terminated when the parent process received termination signals. By installing signal forwarders, the parent now ensures that termination signals are propagated to the child, preventing it from becoming an orphan process. The implementation includes robust cleanup logic to ensure that signal listeners are properly removed, maintaining system stability and preventing resource leaks.

Highlights

  • Signal Forwarding: Implemented signal forwarding for standard termination signals (SIGTERM, SIGHUP, etc.) from the parent process to the relaunched child process to prevent child orphaning.
  • Resource Cleanup: Added a cleanup mechanism to remove signal listeners upon child process termination or error, preventing potential memory leaks and listener count warnings.
  • Testing: Added comprehensive unit tests to verify correct signal propagation and listener cleanup behavior.
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 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 counter productive. 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 in relaunchAppInChildProcess to ensure child processes are terminated when the parent receives termination signals, along with necessary listener cleanup and unit tests. A review comment points out that forwarding SIGINT and SIGQUIT in interactive TTY sessions causes double-delivery of signals and changes the parent's termination behavior, potentially leading to terminal hangs; it suggests refining the logic and updating the documentation.

Comment thread packages/cli/src/utils/relaunch.ts Outdated
@gemini-cli gemini-cli Bot added area/core Issues related to User Interface, OS Support, Core Functionality help wanted We will accept PRs from all issues marked as "help wanted". Thanks for your support! labels Apr 18, 2026
@Sway-Chan
Sway-Chan force-pushed the fix/relaunch-signal-forward branch from bcc87f7 to 016e442 Compare April 18, 2026 01:43
@Sway-Chan

Copy link
Copy Markdown
Author

/gemini review

@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 process to the child process in the relaunchAppInChildProcess utility to prevent orphaned processes. It includes logic to clean up signal listeners when the child process terminates or encounters an error, along with new unit tests covering these scenarios. Feedback was provided regarding the potential for process leaks when SIGINT and SIGQUIT are conditionally bypassed in TTY mode, as well as a potential listener leak if an error occurs during IPC initialization before listeners are attached.

Comment thread packages/cli/src/utils/relaunch.ts Outdated
Comment thread packages/cli/src/utils/relaunch.ts Outdated
relaunchAppInChildProcess spawned a full child but did not install
signal handlers on the parent. When a supervisor (ACP client, systemd,
container runtime) signalled the parent PID, the child was reparented
to PID 1 / the user's systemd manager and continued to hold the
OAuth session and allocated heap.

Install forwarders for SIGTERM/HUP/INT/QUIT/USR1/USR2 before awaiting
the child, and remove them on close/error to avoid listener leaks
across relaunch iterations.

Closes google-gemini#25590
@Sway-Chan
Sway-Chan force-pushed the fix/relaunch-signal-forward branch from 016e442 to 4804bf8 Compare April 18, 2026 09:17
@Sway-Chan

Copy link
Copy Markdown
Author

Thanks for the detailed re-review. I've adopted both suggestions in 4804bf8b2:

  1. Removed the TTY-based exclusion for SIGINT/SIGQUIT — all six signals now forward unconditionally. Your reasoning is correct: kill -INT <parent_pid> (and equivalent programmatic signals from systemd, container runtimes, or supervising ACP clients) only targets the parent, so TTY-based bypass would reintroduce the orphan leak in exactly those cases. Terminal-generated double-delivery is harmless for typical SIGINT/SIGQUIT handlers and strictly preferable to orphaning.

  2. Moved listener attachment after child.send() — forwarders are now registered only after the IPC setup call, so a synchronous throw from child.send cannot leave listeners leaked on the parent process.

Updated the unit tests to match (removed the TTY-skip test, added a listener-leak-on-IPC-throw test). npm run test -- relaunch.test.ts passes 11/11.

@Sway-Chan

Copy link
Copy Markdown
Author

/gemini review

@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 in the relaunchAppInChildProcess utility to ensure that termination signals sent to the parent process are correctly propagated to the child process, preventing orphaned processes. It introduces a mechanism to manage and clean up signal listeners on the parent process during the child's lifecycle, specifically handling error and close events. Additionally, new unit tests have been added to verify signal forwarding, listener cleanup, and resilience against synchronous IPC errors. I have no feedback to provide.

@Sway-Chan

Copy link
Copy Markdown
Author

Gentle ping for maintainer review.

Current status:

  • CLA passes.
  • Gemini Code Assist latest review has no remaining feedback.
  • The latest patch removes the TTY SIGINT/SIGQUIT exclusion and moves signal listener registration after child.send() to avoid listener leaks.
  • Targeted test passes: npm run test -- relaunch.test.ts (11/11).

This PR fixes the orphaned relaunched child process when the parent receives programmatic termination signals.

@gemini-cli gemini-cli Bot added the priority/p2 Important but can be addressed in a future release. label May 7, 2026
// Should default to exit code 1
expect(processExitSpy).toHaveBeenCalledWith(1);
});

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.

Try using parameterized tests and consider refactoring the signal forwarding tests to use it.each. Add vi.restoreAllMocks() to the afterEach block to prevent side effects. Additionally I think there's a redundant signal list in the tests

@spencer426

Copy link
Copy Markdown
Contributor

Thank you for your interest in contributing to the project! We are closing this PR due to inactivity.

@spencer426 spencer426 closed this May 27, 2026
@sripasg sripasg added the size/m A medium sized PR label Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality help wanted We will accept PRs from all issues marked as "help wanted". Thanks for your support! priority/p2 Important but can be addressed in a future release. size/m A medium sized 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

3 participants