mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 14:04:41 -07:00
Add timeout for shell-utils to prevent hangs. (#16667)
This commit is contained in:
@@ -44,6 +44,16 @@ vi.mock('shell-quote', () => ({
|
|||||||
quote: mockQuote,
|
quote: mockQuote,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockDebugLogger = vi.hoisted(() => ({
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('./debugLogger.js', () => ({
|
||||||
|
debugLogger: mockDebugLogger,
|
||||||
|
}));
|
||||||
|
|
||||||
const isWindowsRuntime = process.platform === 'win32';
|
const isWindowsRuntime = process.platform === 'win32';
|
||||||
const describeWindowsOnly = isWindowsRuntime ? describe : describe.skip;
|
const describeWindowsOnly = isWindowsRuntime ? describe : describe.skip;
|
||||||
|
|
||||||
@@ -161,6 +171,28 @@ describe('getCommandRoots', () => {
|
|||||||
const roots = shellUtils.getCommandRoots('ls -la');
|
const roots = shellUtils.getCommandRoots('ls -la');
|
||||||
expect(roots).toEqual([]);
|
expect(roots).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle bash parser timeouts', () => {
|
||||||
|
const nowSpy = vi.spyOn(performance, 'now');
|
||||||
|
// Mock performance.now() to trigger timeout:
|
||||||
|
// 1st call: start time = 0. deadline = 0 + 1000ms.
|
||||||
|
// 2nd call (and onwards): inside progressCallback, return 2000ms.
|
||||||
|
nowSpy.mockReturnValueOnce(0).mockReturnValue(2000);
|
||||||
|
|
||||||
|
// Use a very complex command to ensure progressCallback is triggered at least once
|
||||||
|
const complexCommand =
|
||||||
|
'ls -la && ' + Array(100).fill('echo "hello"').join(' && ');
|
||||||
|
const roots = getCommandRoots(complexCommand);
|
||||||
|
expect(roots).toEqual([]);
|
||||||
|
expect(nowSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(mockDebugLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Bash command parsing timed out for command:',
|
||||||
|
complexCommand,
|
||||||
|
);
|
||||||
|
|
||||||
|
nowSpy.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hasRedirection', () => {
|
describe('hasRedirection', () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
spawnSync,
|
spawnSync,
|
||||||
type SpawnOptionsWithoutStdio,
|
type SpawnOptionsWithoutStdio,
|
||||||
} from 'node:child_process';
|
} from 'node:child_process';
|
||||||
import type { Node } from 'web-tree-sitter';
|
import type { Node, Tree } from 'web-tree-sitter';
|
||||||
import { Language, Parser, Query } from 'web-tree-sitter';
|
import { Language, Parser, Query } from 'web-tree-sitter';
|
||||||
import { loadWasmBinary } from './fileUtils.js';
|
import { loadWasmBinary } from './fileUtils.js';
|
||||||
import { debugLogger } from './debugLogger.js';
|
import { debugLogger } from './debugLogger.js';
|
||||||
@@ -119,6 +119,7 @@ interface CommandParseResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const POWERSHELL_COMMAND_ENV = '__GCLI_POWERSHELL_COMMAND__';
|
const POWERSHELL_COMMAND_ENV = '__GCLI_POWERSHELL_COMMAND__';
|
||||||
|
const PARSE_TIMEOUT_MICROS = 1000 * 1000; // 1 second
|
||||||
|
|
||||||
// Encode the parser script as UTF-16LE base64 so we can pass it via PowerShell's -EncodedCommand flag;
|
// Encode the parser script as UTF-16LE base64 so we can pass it via PowerShell's -EncodedCommand flag;
|
||||||
// this avoids brittle quoting/escaping when spawning PowerShell and ensures the script is received byte-for-byte.
|
// this avoids brittle quoting/escaping when spawning PowerShell and ensures the script is received byte-for-byte.
|
||||||
@@ -179,14 +180,35 @@ function createParser(): Parser | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCommandTree(command: string) {
|
function parseCommandTree(
|
||||||
|
command: string,
|
||||||
|
timeoutMicros: number = PARSE_TIMEOUT_MICROS,
|
||||||
|
): Tree | null {
|
||||||
const parser = createParser();
|
const parser = createParser();
|
||||||
if (!parser || !command.trim()) {
|
if (!parser || !command.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deadline = performance.now() + timeoutMicros / 1000;
|
||||||
|
let timedOut = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return parser.parse(command);
|
const tree = parser.parse(command, null, {
|
||||||
|
progressCallback: () => {
|
||||||
|
if (performance.now() > deadline) {
|
||||||
|
timedOut = true;
|
||||||
|
return true as unknown as void; // Returning true cancels parsing, but type says void
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
debugLogger.error('Bash command parsing timed out for command:', command);
|
||||||
|
// Returning a partial tree could be risky so we return null to be safe.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user