mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
perf(core): optimize Windows IDE process detection from O(N) to O(1) (#11048)
This commit is contained in:
@@ -65,132 +65,118 @@ describe('getIdeProcessInfo', () => {
|
|||||||
describe('on Windows', () => {
|
describe('on Windows', () => {
|
||||||
it('should traverse up and find the great-grandchild of the root process', async () => {
|
it('should traverse up and find the great-grandchild of the root process', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
const processInfoMap = new Map([
|
// process (1000) -> powershell (900) -> code (800) -> wininit (700) -> root (0)
|
||||||
[
|
// Ancestors: [1000, 900, 800, 700]
|
||||||
1000,
|
// Target (great-grandchild of root): 900
|
||||||
|
const processes = [
|
||||||
{
|
{
|
||||||
stdout:
|
ProcessId: 1000,
|
||||||
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}',
|
ParentProcessId: 900,
|
||||||
|
Name: 'node.exe',
|
||||||
|
CommandLine: 'node.exe',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[
|
|
||||||
900,
|
|
||||||
{
|
{
|
||||||
stdout:
|
ProcessId: 900,
|
||||||
'{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}',
|
ParentProcessId: 800,
|
||||||
|
Name: 'powershell.exe',
|
||||||
|
CommandLine: 'powershell.exe',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[
|
|
||||||
800,
|
|
||||||
{
|
{
|
||||||
stdout:
|
ProcessId: 800,
|
||||||
'{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}',
|
ParentProcessId: 700,
|
||||||
|
Name: 'code.exe',
|
||||||
|
CommandLine: 'code.exe',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
[
|
|
||||||
700,
|
|
||||||
{
|
{
|
||||||
stdout:
|
ProcessId: 700,
|
||||||
'{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}',
|
ParentProcessId: 0,
|
||||||
|
Name: 'wininit.exe',
|
||||||
|
CommandLine: 'wininit.exe',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
]);
|
mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });
|
||||||
mockedExec.mockImplementation((command: string) => {
|
|
||||||
const pidMatch = command.match(/ProcessId=(\d+)/);
|
|
||||||
if (pidMatch) {
|
|
||||||
const pid = parseInt(pidMatch[1], 10);
|
|
||||||
return Promise.resolve(processInfoMap.get(pid));
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('Invalid command for mock'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
|
expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
|
||||||
|
expect(mockedExec).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Get-CimInstance Win32_Process'),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-existent process gracefully', async () => {
|
it('should handle short process chains', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
mockedExec
|
// process (1000) -> root (0)
|
||||||
.mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue
|
const processes = [
|
||||||
.mockResolvedValueOnce({
|
{
|
||||||
stdout:
|
ProcessId: 1000,
|
||||||
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
|
ParentProcessId: 0,
|
||||||
}); // Fallback call
|
Name: 'node.exe',
|
||||||
|
CommandLine: 'node.exe',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' });
|
expect(result).toEqual({ pid: 1000, command: 'node.exe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PowerShell failure gracefully', async () => {
|
||||||
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
|
mockedExec.mockRejectedValueOnce(new Error('PowerShell failed'));
|
||||||
|
// Fallback to getProcessInfo for current PID
|
||||||
|
mockedExec.mockResolvedValueOnce({ stdout: '' }); // ps command fails on windows
|
||||||
|
|
||||||
|
const result = await getIdeProcessInfo();
|
||||||
|
expect(result).toEqual({ pid: 1000, command: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle malformed JSON output gracefully', async () => {
|
it('should handle malformed JSON output gracefully', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
mockedExec
|
mockedExec.mockResolvedValueOnce({ stdout: '{"invalid":json}' });
|
||||||
.mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON
|
// Fallback to getProcessInfo for current PID
|
||||||
.mockResolvedValueOnce({
|
mockedExec.mockResolvedValueOnce({ stdout: '' });
|
||||||
stdout:
|
|
||||||
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
|
|
||||||
}); // Fallback call
|
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' });
|
expect(result).toEqual({ pid: 1000, command: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle PowerShell errors without crashing the process chain', async () => {
|
it('should handle single process output from ConvertTo-Json', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
const processInfoMap = new Map([
|
const process = {
|
||||||
[1000, { stdout: '' }], // First process doesn't exist (empty due to -ErrorAction)
|
ProcessId: 1000,
|
||||||
[
|
ParentProcessId: 0,
|
||||||
1001,
|
Name: 'node.exe',
|
||||||
{
|
CommandLine: 'node.exe',
|
||||||
stdout:
|
};
|
||||||
'{"Name":"parent.exe","ParentProcessId":800,"CommandLine":"parent.exe"}',
|
mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(process) });
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
800,
|
|
||||||
{
|
|
||||||
stdout:
|
|
||||||
'{"Name":"ide.exe","ParentProcessId":0,"CommandLine":"ide.exe"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Mock the process.pid to test traversal with missing processes
|
|
||||||
Object.defineProperty(process, 'pid', {
|
|
||||||
value: 1001,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockedExec.mockImplementation((command: string) => {
|
|
||||||
const pidMatch = command.match(/ProcessId=(\d+)/);
|
|
||||||
if (pidMatch) {
|
|
||||||
const pid = parseInt(pidMatch[1], 10);
|
|
||||||
return Promise.resolve(processInfoMap.get(pid) || { stdout: '' });
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('Invalid command for mock'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
// Should return the current process command since traversal continues despite missing processes
|
expect(result).toEqual({ pid: 1000, command: 'node.exe' });
|
||||||
expect(result).toEqual({ pid: 1001, command: 'parent.exe' });
|
|
||||||
|
|
||||||
// Reset process.pid
|
|
||||||
Object.defineProperty(process, 'pid', {
|
|
||||||
value: 1000,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle partial JSON data with defaults', async () => {
|
it('should handle missing process in map during traversal', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
mockedExec
|
// process (1000) -> parent (900) -> missing (800)
|
||||||
.mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0
|
const processes = [
|
||||||
.mockResolvedValueOnce({
|
{
|
||||||
stdout:
|
ProcessId: 1000,
|
||||||
'{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}',
|
ParentProcessId: 900,
|
||||||
}); // Get grandparent info
|
Name: 'node.exe',
|
||||||
|
CommandLine: 'node.exe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ProcessId: 900,
|
||||||
|
ParentProcessId: 800,
|
||||||
|
Name: 'parent.exe',
|
||||||
|
CommandLine: 'parent.exe',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
expect(result).toEqual({ pid: 1000, command: 'root.exe' });
|
// Ancestors: [1000, 900]. Length < 3, returns last (900)
|
||||||
|
expect(result).toEqual({ pid: 900, command: 'parent.exe' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,67 @@ const execAsync = promisify(exec);
|
|||||||
|
|
||||||
const MAX_TRAVERSAL_DEPTH = 32;
|
const MAX_TRAVERSAL_DEPTH = 32;
|
||||||
|
|
||||||
|
interface ProcessInfo {
|
||||||
|
pid: number;
|
||||||
|
parentPid: number;
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawProcessInfo {
|
||||||
|
ProcessId?: number;
|
||||||
|
ParentProcessId?: number;
|
||||||
|
Name?: string;
|
||||||
|
CommandLine?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the parent process ID, name, and command for a given process ID.
|
* Fetches the entire process table on Windows.
|
||||||
|
*/
|
||||||
|
async function getProcessTableWindows(): Promise<Map<number, ProcessInfo>> {
|
||||||
|
const processMap = new Map<number, ProcessInfo>();
|
||||||
|
try {
|
||||||
|
// Fetch ProcessId, ParentProcessId, Name, and CommandLine for all processes.
|
||||||
|
const powershellCommand =
|
||||||
|
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress';
|
||||||
|
// Increase maxBuffer to handle large process lists (default is 1MB)
|
||||||
|
const { stdout } = await execAsync(`powershell "${powershellCommand}"`, {
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stdout.trim()) {
|
||||||
|
return processMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processes: RawProcessInfo | RawProcessInfo[];
|
||||||
|
try {
|
||||||
|
processes = JSON.parse(stdout);
|
||||||
|
} catch (_e) {
|
||||||
|
return processMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(processes)) {
|
||||||
|
processes = [processes];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of processes) {
|
||||||
|
if (p && typeof p.ProcessId === 'number') {
|
||||||
|
processMap.set(p.ProcessId, {
|
||||||
|
pid: p.ProcessId,
|
||||||
|
parentPid: p.ParentProcessId || 0,
|
||||||
|
name: p.Name || '',
|
||||||
|
command: p.CommandLine || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Fallback or error handling if PowerShell fails
|
||||||
|
}
|
||||||
|
return processMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the parent process ID, name, and command for a given process ID on Unix.
|
||||||
*
|
*
|
||||||
* @param pid The process ID to inspect.
|
* @param pid The process ID to inspect.
|
||||||
* @returns A promise that resolves to the parent's PID, name, and command.
|
* @returns A promise that resolves to the parent's PID, name, and command.
|
||||||
@@ -25,49 +84,24 @@ async function getProcessInfo(pid: number): Promise<{
|
|||||||
command: string;
|
command: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const platform = os.platform();
|
|
||||||
if (platform === 'win32') {
|
|
||||||
const powershellCommand = [
|
|
||||||
'$p = Get-CimInstance Win32_Process',
|
|
||||||
`-Filter 'ProcessId=${pid}'`,
|
|
||||||
'-ErrorAction SilentlyContinue;',
|
|
||||||
'if ($p) {',
|
|
||||||
'@{Name=$p.Name;ParentProcessId=$p.ParentProcessId;CommandLine=$p.CommandLine}',
|
|
||||||
'| ConvertTo-Json',
|
|
||||||
'}',
|
|
||||||
].join(' ');
|
|
||||||
const { stdout } = await execAsync(`powershell "${powershellCommand}"`);
|
|
||||||
const output = stdout.trim();
|
|
||||||
if (!output) return { parentPid: 0, name: '', command: '' };
|
|
||||||
const {
|
|
||||||
Name = '',
|
|
||||||
ParentProcessId = 0,
|
|
||||||
CommandLine = '',
|
|
||||||
} = JSON.parse(output);
|
|
||||||
return {
|
|
||||||
parentPid: ParentProcessId,
|
|
||||||
name: Name,
|
|
||||||
command: CommandLine ?? '',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const command = `ps -o ppid=,command= -p ${pid}`;
|
const command = `ps -o ppid=,command= -p ${pid}`;
|
||||||
const { stdout } = await execAsync(command);
|
const { stdout } = await execAsync(command);
|
||||||
const trimmedStdout = stdout.trim();
|
const trimmedStdout = stdout.trim();
|
||||||
if (!trimmedStdout) {
|
if (!trimmedStdout) {
|
||||||
return { parentPid: 0, name: '', command: '' };
|
return { parentPid: 0, name: '', command: '' };
|
||||||
}
|
}
|
||||||
const ppidString = trimmedStdout.split(/\s+/)[0];
|
const parts = trimmedStdout.split(/\s+/);
|
||||||
|
const ppidString = parts[0];
|
||||||
const parentPid = parseInt(ppidString, 10);
|
const parentPid = parseInt(ppidString, 10);
|
||||||
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
|
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
|
||||||
const processName = path.basename(fullCommand.split(' ')[0]);
|
const processName = path.basename(fullCommand.split(' ')[0]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
parentPid: isNaN(parentPid) ? 1 : parentPid,
|
parentPid: isNaN(parentPid) ? 1 : parentPid,
|
||||||
name: processName,
|
name: processName,
|
||||||
command: fullCommand,
|
command: fullCommand,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
console.debug(`Failed to get process info for pid ${pid}:`, _e);
|
|
||||||
return { parentPid: 0, name: '', command: '' };
|
return { parentPid: 0, name: '', command: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,49 +159,45 @@ async function getIdeProcessInfoForUnix(): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the IDE process info on Windows.
|
* Finds the IDE process info on Windows using a snapshot approach.
|
||||||
*
|
|
||||||
* The strategy is to find the great-grandchild of the root process.
|
|
||||||
*
|
|
||||||
* @returns A promise that resolves to the PID and command of the IDE process.
|
|
||||||
*/
|
*/
|
||||||
async function getIdeProcessInfoForWindows(): Promise<{
|
async function getIdeProcessInfoForWindows(): Promise<{
|
||||||
pid: number;
|
pid: number;
|
||||||
command: string;
|
command: string;
|
||||||
}> {
|
}> {
|
||||||
let currentPid = process.pid;
|
// Fetch the entire process table in one go.
|
||||||
let previousPid = process.pid;
|
const processMap = await getProcessTableWindows();
|
||||||
|
const myPid = process.pid;
|
||||||
|
const myProc = processMap.get(myPid);
|
||||||
|
|
||||||
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
if (!myProc) {
|
||||||
try {
|
// Fallback: try to get info for current process directly if snapshot fails
|
||||||
const { parentPid } = await getProcessInfo(currentPid);
|
const { command } = await getProcessInfo(myPid);
|
||||||
|
return { pid: myPid, command };
|
||||||
if (parentPid > 0) {
|
|
||||||
try {
|
|
||||||
const { parentPid: grandParentPid } = await getProcessInfo(parentPid);
|
|
||||||
if (grandParentPid === 0) {
|
|
||||||
// We've found the grandchild of the root (`currentPid`). The IDE
|
|
||||||
// process is its child, which we've stored in `previousPid`.
|
|
||||||
const { command } = await getProcessInfo(previousPid);
|
|
||||||
return { pid: previousPid, command };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// getting grandparent failed, proceed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentPid <= 0) {
|
// Perform tree traversal in memory.
|
||||||
break; // Reached the root
|
// Strategy: Find the great-grandchild of the root process (pid 0 or non-existent parent).
|
||||||
|
const ancestors: ProcessInfo[] = [];
|
||||||
|
let curr: ProcessInfo | undefined = myProc;
|
||||||
|
|
||||||
|
for (let i = 0; i < MAX_TRAVERSAL_DEPTH && curr; i++) {
|
||||||
|
ancestors.push(curr);
|
||||||
|
if (curr.parentPid === 0 || !processMap.has(curr.parentPid)) {
|
||||||
|
break; // Reached root
|
||||||
}
|
}
|
||||||
previousPid = currentPid;
|
curr = processMap.get(curr.parentPid);
|
||||||
currentPid = parentPid;
|
|
||||||
} catch {
|
|
||||||
// Process in chain died
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ancestors.length >= 3) {
|
||||||
|
const target = ancestors[ancestors.length - 3];
|
||||||
|
return { pid: target.pid, command: target.command };
|
||||||
|
} else if (ancestors.length > 0) {
|
||||||
|
const target = ancestors[ancestors.length - 1];
|
||||||
|
return { pid: target.pid, command: target.command };
|
||||||
}
|
}
|
||||||
const { command } = await getProcessInfo(currentPid);
|
|
||||||
return { pid: currentPid, command };
|
return { pid: myPid, command: myProc.command };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user