mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
(fix): Respect ctrl+c signal for aborting execution in NonInteractive mode (#11478)
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
import type { Content, Part } from '@google/genai';
|
import type { Content, Part } from '@google/genai';
|
||||||
|
import readline from 'node:readline';
|
||||||
|
|
||||||
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
||||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||||
@@ -82,9 +83,95 @@ export async function runNonInteractive({
|
|||||||
? new StreamJsonFormatter()
|
? new StreamJsonFormatter()
|
||||||
: null;
|
: 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;
|
let errorToHandle: unknown | undefined;
|
||||||
try {
|
try {
|
||||||
consolePatcher.patch();
|
consolePatcher.patch();
|
||||||
|
|
||||||
|
// Setup stdin cancellation listener
|
||||||
|
setupStdinCancellation();
|
||||||
|
|
||||||
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
|
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
|
||||||
coreEvents.drainFeedbackBacklog();
|
coreEvents.drainFeedbackBacklog();
|
||||||
|
|
||||||
@@ -108,8 +195,6 @@ export async function runNonInteractive({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
let query: Part[] | undefined;
|
let query: Part[] | undefined;
|
||||||
|
|
||||||
if (isSlashCommand(input)) {
|
if (isSlashCommand(input)) {
|
||||||
@@ -336,6 +421,9 @@ export async function runNonInteractive({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorToHandle = error;
|
errorToHandle = error;
|
||||||
} finally {
|
} finally {
|
||||||
|
// Cleanup stdin cancellation before other cleanup
|
||||||
|
cleanupStdinCancellation();
|
||||||
|
|
||||||
consolePatcher.cleanup();
|
consolePatcher.cleanup();
|
||||||
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
|
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
|
||||||
if (isTelemetrySdkInitialized()) {
|
if (isTelemetrySdkInitialized()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user