mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix: optimize windows process tree retrieval to prevent hang
This commit is contained in:
@@ -65,132 +65,93 @@ 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([
|
|
||||||
[
|
const processes = [
|
||||||
1000,
|
{
|
||||||
{
|
ProcessId: 1000,
|
||||||
stdout:
|
ParentProcessId: 900,
|
||||||
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}',
|
Name: 'node.exe',
|
||||||
},
|
CommandLine: 'node.exe',
|
||||||
],
|
},
|
||||||
[
|
{
|
||||||
900,
|
ProcessId: 900,
|
||||||
{
|
ParentProcessId: 800,
|
||||||
stdout:
|
Name: 'powershell.exe',
|
||||||
'{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}',
|
CommandLine: 'powershell.exe',
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
[
|
ProcessId: 800,
|
||||||
800,
|
ParentProcessId: 700,
|
||||||
{
|
Name: 'code.exe',
|
||||||
stdout:
|
CommandLine: 'code.exe',
|
||||||
'{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}',
|
},
|
||||||
},
|
{
|
||||||
],
|
ProcessId: 700,
|
||||||
[
|
ParentProcessId: 0,
|
||||||
700,
|
Name: 'wininit.exe',
|
||||||
{
|
CommandLine: 'wininit.exe',
|
||||||
stdout:
|
},
|
||||||
'{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}',
|
];
|
||||||
},
|
|
||||||
],
|
mockedExec.mockImplementation((file: string, _args: string[]) => {
|
||||||
]);
|
if (file === 'powershell') {
|
||||||
mockedExec.mockImplementation((command: string) => {
|
return Promise.resolve({ stdout: JSON.stringify(processes) });
|
||||||
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'));
|
// Fallback for getProcessInfo calls if any (should not happen in new logic for Windows traversal)
|
||||||
|
return Promise.resolve({ stdout: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
const result = await getIdeProcessInfo();
|
||||||
|
// 1000 -> 900 -> 800 -> 700 (root child)
|
||||||
|
// Great-grandchild of root (700) is 900.
|
||||||
|
// Wait, logic is:
|
||||||
|
// 700 (root child) -> 800 (grandchild) -> 900 (great-grandchild) -> 1000 (current)
|
||||||
|
// The code looks for the grandchild of the root.
|
||||||
|
// Root is 0 (conceptually). Child of root is 700. Grandchild is 800.
|
||||||
|
// The code says:
|
||||||
|
// "We've found the grandchild of the root (`currentPid`). The IDE process is its child, which we've stored in `previousPid`."
|
||||||
|
|
||||||
|
// Let's trace the loop in `getIdeProcessInfoForWindows`:
|
||||||
|
// currentPid = 1000, previousPid = 1000
|
||||||
|
// Loop 1:
|
||||||
|
// proc = 1000. parentPid = 900.
|
||||||
|
// parentProc = 900. parentProc.parentPid = 800 != 0.
|
||||||
|
// previousPid = 1000. currentPid = 900.
|
||||||
|
// Loop 2:
|
||||||
|
// proc = 900. parentPid = 800.
|
||||||
|
// parentProc = 800. parentProc.parentPid = 700 != 0.
|
||||||
|
// previousPid = 900. currentPid = 800.
|
||||||
|
// Loop 3:
|
||||||
|
// proc = 800. parentPid = 700.
|
||||||
|
// parentProc = 700. parentProc.parentPid = 0.
|
||||||
|
// MATCH!
|
||||||
|
// ideProc = processMap.get(previousPid) = processMap.get(900) = powershell.exe
|
||||||
|
// return { pid: 900, command: 'powershell.exe' }
|
||||||
|
|
||||||
expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
|
expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-existent process gracefully', async () => {
|
it('should handle empty process list gracefully', async () => {
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
(os.platform as Mock).mockReturnValue('win32');
|
||||||
mockedExec
|
mockedExec.mockResolvedValue({ stdout: '[]' });
|
||||||
.mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
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' });
|
// Should return current pid and empty command because process not found in map
|
||||||
|
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.mockResolvedValue({ stdout: '{"invalid":json}' }); // Malformed JSON will throw in JSON.parse
|
||||||
.mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON
|
|
||||||
.mockResolvedValueOnce({
|
// If JSON.parse fails, getProcessTreeForWindows returns empty map.
|
||||||
stdout:
|
// Then getIdeProcessInfoForWindows returns current pid.
|
||||||
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
|
|
||||||
}); // Fallback call
|
// Wait, mockedExec throws? No, JSON.parse throws.
|
||||||
|
// getProcessTreeForWindows catches error and returns empty map.
|
||||||
|
|
||||||
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 () => {
|
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
|
||||||
const processInfoMap = new Map([
|
|
||||||
[1000, { stdout: '' }], // First process doesn't exist (empty due to -ErrorAction)
|
|
||||||
[
|
|
||||||
1001,
|
|
||||||
{
|
|
||||||
stdout:
|
|
||||||
'{"Name":"parent.exe","ParentProcessId":800,"CommandLine":"parent.exe"}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
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();
|
|
||||||
// Should return the current process command since traversal continues despite missing processes
|
|
||||||
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 () => {
|
|
||||||
(os.platform as Mock).mockReturnValue('win32');
|
|
||||||
mockedExec
|
|
||||||
.mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
stdout:
|
|
||||||
'{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}',
|
|
||||||
}); // Get grandparent info
|
|
||||||
|
|
||||||
const result = await getIdeProcessInfo();
|
|
||||||
expect(result).toEqual({ pid: 1000, command: 'root.exe' });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec } from 'node:child_process';
|
import { exec, execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
const MAX_TRAVERSAL_DEPTH = 32;
|
const MAX_TRAVERSAL_DEPTH = 32;
|
||||||
|
|
||||||
@@ -36,7 +37,13 @@ async function getProcessInfo(pid: number): Promise<{
|
|||||||
'| ConvertTo-Json',
|
'| ConvertTo-Json',
|
||||||
'}',
|
'}',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
const { stdout } = await execAsync(`powershell "${powershellCommand}"`);
|
|
||||||
|
const { stdout } = await execFileAsync('powershell', [
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-Command',
|
||||||
|
powershellCommand,
|
||||||
|
]);
|
||||||
const output = stdout.trim();
|
const output = stdout.trim();
|
||||||
if (!output) return { parentPid: 0, name: '', command: '' };
|
if (!output) return { parentPid: 0, name: '', command: '' };
|
||||||
const {
|
const {
|
||||||
@@ -124,6 +131,37 @@ async function getIdeProcessInfoForUnix(): Promise<{
|
|||||||
return { pid: currentPid, command };
|
return { pid: currentPid, command };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getProcessTreeForWindows(): Promise<
|
||||||
|
Map<number, { parentPid: number; name: string; command: string }>
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const powershellCommand =
|
||||||
|
'Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, Name, CommandLine | ConvertTo-Json -Depth 1';
|
||||||
|
const { stdout } = await execFileAsync(
|
||||||
|
'powershell',
|
||||||
|
['-NoProfile', '-NonInteractive', '-Command', powershellCommand],
|
||||||
|
{ maxBuffer: 10 * 1024 * 1024 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const processes = JSON.parse(stdout);
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
const list = Array.isArray(processes) ? processes : [processes];
|
||||||
|
|
||||||
|
for (const p of list) {
|
||||||
|
map.set(p.ProcessId, {
|
||||||
|
parentPid: p.ParentProcessId,
|
||||||
|
name: p.Name,
|
||||||
|
command: p.CommandLine || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get process tree:', e);
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the IDE process info on Windows.
|
* Finds the IDE process info on Windows.
|
||||||
*
|
*
|
||||||
@@ -135,39 +173,33 @@ async function getIdeProcessInfoForWindows(): Promise<{
|
|||||||
pid: number;
|
pid: number;
|
||||||
command: string;
|
command: string;
|
||||||
}> {
|
}> {
|
||||||
|
const processMap = await getProcessTreeForWindows();
|
||||||
let currentPid = process.pid;
|
let currentPid = process.pid;
|
||||||
let previousPid = process.pid;
|
let previousPid = process.pid;
|
||||||
|
|
||||||
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
||||||
try {
|
const proc = processMap.get(currentPid);
|
||||||
const { parentPid } = await getProcessInfo(currentPid);
|
if (!proc) break;
|
||||||
|
|
||||||
if (parentPid > 0) {
|
const parentPid = proc.parentPid;
|
||||||
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) {
|
if (parentPid > 0) {
|
||||||
break; // Reached the root
|
const parentProc = processMap.get(parentPid);
|
||||||
|
if (parentProc && parentProc.parentPid === 0) {
|
||||||
|
// We've found the grandchild of the root (`currentPid`). The IDE
|
||||||
|
// process is its child, which we've stored in `previousPid`.
|
||||||
|
const ideProc = processMap.get(previousPid);
|
||||||
|
return { pid: previousPid, command: ideProc?.command || '' };
|
||||||
}
|
}
|
||||||
previousPid = currentPid;
|
|
||||||
currentPid = parentPid;
|
|
||||||
} catch {
|
|
||||||
// Process in chain died
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parentPid <= 0) break;
|
||||||
|
previousPid = currentPid;
|
||||||
|
currentPid = parentPid;
|
||||||
}
|
}
|
||||||
const { command } = await getProcessInfo(currentPid);
|
|
||||||
return { pid: currentPid, command };
|
const currentProc = processMap.get(currentPid);
|
||||||
|
return { pid: currentPid, command: currentProc?.command || '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user