mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 09:01:17 -07:00
fix(core): fix PTY descriptor shell leak (#16773)
This commit is contained in:
@@ -20,7 +20,9 @@ import {
|
||||
initializeShellParsers,
|
||||
stripShellWrapper,
|
||||
hasRedirection,
|
||||
resolveExecutable,
|
||||
} from './shell-utils.js';
|
||||
import path from 'node:path';
|
||||
|
||||
const mockPlatform = vi.hoisted(() => vi.fn());
|
||||
const mockHomedir = vi.hoisted(() => vi.fn());
|
||||
@@ -33,6 +35,20 @@ vi.mock('os', () => ({
|
||||
homedir: mockHomedir,
|
||||
}));
|
||||
|
||||
const mockAccess = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:fs', () => ({
|
||||
default: {
|
||||
promises: {
|
||||
access: mockAccess,
|
||||
},
|
||||
constants: { X_OK: 1 },
|
||||
},
|
||||
promises: {
|
||||
access: mockAccess,
|
||||
},
|
||||
constants: { X_OK: 1 },
|
||||
}));
|
||||
|
||||
const mockSpawnSync = vi.hoisted(() => vi.fn());
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawnSync: mockSpawnSync,
|
||||
@@ -463,3 +479,65 @@ describe('hasRedirection (PowerShell via mock)', () => {
|
||||
expect(hasRedirection('echo "-> arrow"')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveExecutable', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
mockAccess.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return the absolute path if it exists and is executable', async () => {
|
||||
const absPath = path.resolve('/usr/bin/git');
|
||||
mockAccess.mockResolvedValue(undefined); // success
|
||||
expect(await resolveExecutable(absPath)).toBe(absPath);
|
||||
expect(mockAccess).toHaveBeenCalledWith(absPath, 1);
|
||||
});
|
||||
|
||||
it('should return undefined for absolute path if it does not exist', async () => {
|
||||
const absPath = path.resolve('/usr/bin/nonexistent');
|
||||
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
||||
expect(await resolveExecutable(absPath)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve executable in PATH', async () => {
|
||||
const binDir = path.resolve('/bin');
|
||||
const usrBinDir = path.resolve('/usr/bin');
|
||||
process.env['PATH'] = `${binDir}${path.delimiter}${usrBinDir}`;
|
||||
mockPlatform.mockReturnValue('linux');
|
||||
|
||||
const targetPath = path.join(usrBinDir, 'ls');
|
||||
mockAccess.mockImplementation(async (p: string) => {
|
||||
if (p === targetPath) return undefined;
|
||||
throw new Error('ENOENT');
|
||||
});
|
||||
|
||||
expect(await resolveExecutable('ls')).toBe(targetPath);
|
||||
});
|
||||
|
||||
it('should try extensions on Windows', async () => {
|
||||
const sys32 = path.resolve('C:\\Windows\\System32');
|
||||
process.env['PATH'] = sys32;
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
mockAccess.mockImplementation(async (p: string) => {
|
||||
// Use includes because on Windows path separators might differ
|
||||
if (p.includes('cmd.exe')) return undefined;
|
||||
throw new Error('ENOENT');
|
||||
});
|
||||
|
||||
expect(await resolveExecutable('cmd')).toContain('cmd.exe');
|
||||
});
|
||||
|
||||
it('should return undefined if not found in PATH', async () => {
|
||||
process.env['PATH'] = path.resolve('/bin');
|
||||
mockPlatform.mockReturnValue('linux');
|
||||
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
expect(await resolveExecutable('unknown')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { quote } from 'shell-quote';
|
||||
import {
|
||||
spawn,
|
||||
@@ -37,6 +39,35 @@ export interface ShellConfiguration {
|
||||
shell: ShellType;
|
||||
}
|
||||
|
||||
export async function resolveExecutable(
|
||||
exe: string,
|
||||
): Promise<string | undefined> {
|
||||
if (path.isAbsolute(exe)) {
|
||||
try {
|
||||
await fs.promises.access(exe, fs.constants.X_OK);
|
||||
return exe;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const paths = (process.env['PATH'] || '').split(path.delimiter);
|
||||
const extensions =
|
||||
os.platform() === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
||||
|
||||
for (const p of paths) {
|
||||
for (const ext of extensions) {
|
||||
const fullPath = path.join(p, exe + ext);
|
||||
try {
|
||||
await fs.promises.access(fullPath, fs.constants.X_OK);
|
||||
return fullPath;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let bashLanguage: Language | null = null;
|
||||
let treeSitterInitialization: Promise<void> | null = null;
|
||||
let treeSitterInitializationError: Error | null = null;
|
||||
|
||||
Reference in New Issue
Block a user