(fix): Respect ctrl+c signal for aborting execution in NonInteractive mode (#11478)

This commit is contained in:
anj-s
2025-11-03 13:49:01 -08:00
committed by GitHub
parent 4fc9b1cde2
commit 1c044ba8af

View File

@@ -30,6 +30,7 @@ import {
} from '@google/gemini-cli-core';
import type { Content, Part } from '@google/genai';
import readline from 'node:readline';
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
@@ -82,9 +83,95 @@ export async function runNonInteractive({
? new StreamJsonFormatter()
: null;
const abortController = new AbortController();
// Track cancellation state
let isAborting = false;
let cancelMessageTimer: NodeJS.Timeout | null = null;
// Setup stdin listener for Ctrl+C detection
let stdinWasRaw = false;
let rl: readline.Interface | null = null;
const setupStdinCancellation = () => {
// Only setup if stdin is a TTY (user can interact)
if (!process.stdin.isTTY) {
return;
}
// Save original raw mode state
stdinWasRaw = process.stdin.isRaw || false;
// Enable raw mode to capture individual keypresses
process.stdin.setRawMode(true);
process.stdin.resume();
// Setup readline to emit keypress events
rl = readline.createInterface({
input: process.stdin,
escapeCodeTimeout: 0,
});
readline.emitKeypressEvents(process.stdin, rl);
// Listen for Ctrl+C
const keypressHandler = (
str: string,
key: { name?: string; ctrl?: boolean },
) => {
// Detect Ctrl+C: either ctrl+c key combo or raw character code 3
if ((key && key.ctrl && key.name === 'c') || str === '\u0003') {
// Only handle once
if (isAborting) {
return;
}
isAborting = true;
// Only show message if cancellation takes longer than 200ms
// This reduces verbosity for fast cancellations
cancelMessageTimer = setTimeout(() => {
process.stderr.write('\nCancelling...\n');
}, 200);
abortController.abort();
// Note: Don't exit here - let the abort flow through the system
// and trigger handleCancellationError() which will exit with proper code
}
};
process.stdin.on('keypress', keypressHandler);
};
const cleanupStdinCancellation = () => {
// Clear any pending cancel message timer
if (cancelMessageTimer) {
clearTimeout(cancelMessageTimer);
cancelMessageTimer = null;
}
// Cleanup readline and stdin listeners
if (rl) {
rl.close();
rl = null;
}
// Remove keypress listener
process.stdin.removeAllListeners('keypress');
// Restore stdin to original state
if (process.stdin.isTTY) {
process.stdin.setRawMode(stdinWasRaw);
process.stdin.pause();
}
};
let errorToHandle: unknown | undefined;
try {
consolePatcher.patch();
// Setup stdin cancellation listener
setupStdinCancellation();
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
coreEvents.drainFeedbackBacklog();
@@ -108,8 +195,6 @@ export async function runNonInteractive({
});
}
const abortController = new AbortController();
let query: Part[] | undefined;
if (isSlashCommand(input)) {
@@ -336,6 +421,9 @@ export async function runNonInteractive({
} catch (error) {
errorToHandle = error;
} finally {
// Cleanup stdin cancellation before other cleanup
cleanupStdinCancellation();
consolePatcher.cleanup();
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
if (isTelemetrySdkInitialized()) {