From 000cbc25b8cc7a4b500fe19fcb76d7f246d30dc5 Mon Sep 17 00:00:00 2001 From: Juan Date: Sun, 4 Jan 2026 14:32:31 -0300 Subject: [PATCH] fix(cli): prevent orphaned processes from consuming 100% CPU Added SIGHUP, SIGTERM, and SIGINT signal handlers to ensure graceful shutdown and resource cleanup. Added a periodic TTY check for interactive sessions as a defense-in-depth measure against revoked terminal file descriptors causing infinite spin loops. Fixes #15874 --- packages/cli/src/gemini.tsx | 53 ++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index eacef49cb3f..1c403cb8243 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -176,6 +176,52 @@ ${reason.stack}` }); } +/** + * Sets up signal handlers to ensure graceful shutdown and prevent orphaned + * processes, especially when the terminal is closed (SIGHUP). + */ +function setupSignalHandlers() { + const gracefulShutdown = async (signal: string) => { + debugLogger.debug(`Received ${signal}, shutting down gracefully...`); + try { + await runExitCleanup(); + } catch (_) { + // Ignore errors during cleanup + } + process.exit(ExitCodes.SUCCESS); + }; + + process.on('SIGHUP', () => gracefulShutdown('SIGHUP')); + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); +} + +/** + * Sets up a periodic TTY check to detect if we've lost our controlling terminal + * but somehow didn't receive or process SIGHUP. + * This is a defense-in-depth measure against CPU-spinning orphans. + */ +function setupTtyCheck() { + const ttyCheckInterval = setInterval(async () => { + if ( + !process.stdin.isTTY && + !process.stdout.isTTY && + !process.env['SANDBOX'] + ) { + debugLogger.warn('Lost controlling terminal, exiting...'); + try { + await runExitCleanup(); + } catch (_) { + // Ignore errors during cleanup + } + process.exit(ExitCodes.SUCCESS); + } + }, 5000); + + // Don't keep the process alive just for this interval + ttyCheckInterval.unref(); +} + export async function startInteractiveUI( config: Config, settings: LoadedSettings, @@ -300,6 +346,7 @@ export async function main() { }); setupUnhandledRejectionHandler(); + setupSignalHandlers(); const loadSettingsHandle = startupProfiler.start('load_settings'); const settings = loadSettings(); loadSettingsHandle?.end(); @@ -561,10 +608,7 @@ export async function main() { } // This cleanup isn't strictly needed but may help in certain situations. - process.on('SIGTERM', () => { - process.stdin.setRawMode(wasRaw); - }); - process.on('SIGINT', () => { + registerSyncCleanup(() => { process.stdin.setRawMode(wasRaw); }); } @@ -620,6 +664,7 @@ export async function main() { cliStartupHandle?.end(); // Render UI, passing necessary config values. Check that there is no command line question. if (config.isInteractive()) { + setupTtyCheck(); await startInteractiveUI( config, settings,