mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-23 10:17:50 +00:00
Compare commits
5 Commits
release/v0
...
cb59b5a9dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb59b5a9dc | ||
|
|
01e62a2120 | ||
|
|
7eabf543b4 | ||
|
|
f47c762620 | ||
|
|
4f2b2d0a3e |
@@ -65,132 +65,93 @@ describe('getIdeProcessInfo', () => {
|
||||
describe('on Windows', () => {
|
||||
it('should traverse up and find the great-grandchild of the root process', async () => {
|
||||
(os.platform as Mock).mockReturnValue('win32');
|
||||
const processInfoMap = new Map([
|
||||
[
|
||||
1000,
|
||||
{
|
||||
stdout:
|
||||
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}',
|
||||
},
|
||||
],
|
||||
[
|
||||
900,
|
||||
{
|
||||
stdout:
|
||||
'{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}',
|
||||
},
|
||||
],
|
||||
[
|
||||
800,
|
||||
{
|
||||
stdout:
|
||||
'{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}',
|
||||
},
|
||||
],
|
||||
[
|
||||
700,
|
||||
{
|
||||
stdout:
|
||||
'{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}',
|
||||
},
|
||||
],
|
||||
]);
|
||||
mockedExec.mockImplementation((command: string) => {
|
||||
const pidMatch = command.match(/ProcessId=(\d+)/);
|
||||
if (pidMatch) {
|
||||
const pid = parseInt(pidMatch[1], 10);
|
||||
return Promise.resolve(processInfoMap.get(pid));
|
||||
|
||||
const processes = [
|
||||
{
|
||||
ProcessId: 1000,
|
||||
ParentProcessId: 900,
|
||||
Name: 'node.exe',
|
||||
CommandLine: 'node.exe',
|
||||
},
|
||||
{
|
||||
ProcessId: 900,
|
||||
ParentProcessId: 800,
|
||||
Name: 'powershell.exe',
|
||||
CommandLine: 'powershell.exe',
|
||||
},
|
||||
{
|
||||
ProcessId: 800,
|
||||
ParentProcessId: 700,
|
||||
Name: 'code.exe',
|
||||
CommandLine: 'code.exe',
|
||||
},
|
||||
{
|
||||
ProcessId: 700,
|
||||
ParentProcessId: 0,
|
||||
Name: 'wininit.exe',
|
||||
CommandLine: 'wininit.exe',
|
||||
},
|
||||
];
|
||||
|
||||
mockedExec.mockImplementation((file: string, _args: string[]) => {
|
||||
if (file === 'powershell') {
|
||||
return Promise.resolve({ stdout: JSON.stringify(processes) });
|
||||
}
|
||||
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();
|
||||
// 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' });
|
||||
});
|
||||
|
||||
it('should handle non-existent process gracefully', async () => {
|
||||
it('should handle empty process list gracefully', async () => {
|
||||
(os.platform as Mock).mockReturnValue('win32');
|
||||
mockedExec
|
||||
.mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue
|
||||
.mockResolvedValueOnce({
|
||||
stdout:
|
||||
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
|
||||
}); // Fallback call
|
||||
mockedExec.mockResolvedValue({ stdout: '[]' });
|
||||
|
||||
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 () => {
|
||||
(os.platform as Mock).mockReturnValue('win32');
|
||||
mockedExec
|
||||
.mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON
|
||||
.mockResolvedValueOnce({
|
||||
stdout:
|
||||
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
|
||||
}); // Fallback call
|
||||
mockedExec.mockResolvedValue({ stdout: '{"invalid":json}' }); // Malformed JSON will throw in JSON.parse
|
||||
|
||||
// If JSON.parse fails, getProcessTreeForWindows returns empty map.
|
||||
// Then getIdeProcessInfoForWindows returns current pid.
|
||||
|
||||
// Wait, mockedExec throws? No, JSON.parse throws.
|
||||
// getProcessTreeForWindows catches error and returns empty map.
|
||||
|
||||
const result = await getIdeProcessInfo();
|
||||
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' });
|
||||
});
|
||||
|
||||
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' });
|
||||
expect(result).toEqual({ pid: 1000, command: '' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
* 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 os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const MAX_TRAVERSAL_DEPTH = 32;
|
||||
|
||||
@@ -36,7 +37,10 @@ async function getProcessInfo(pid: number): Promise<{
|
||||
'| ConvertTo-Json',
|
||||
'}',
|
||||
].join(' ');
|
||||
const { stdout } = await execAsync(`powershell "${powershellCommand}"`);
|
||||
|
||||
const { stdout } = await execAsync(
|
||||
`powershell -NoProfile -NonInteractive -Command "${powershellCommand.replace(/"/g, '\\"')}"`,
|
||||
);
|
||||
const output = stdout.trim();
|
||||
if (!output) return { parentPid: 0, name: '', command: '' };
|
||||
const {
|
||||
@@ -106,15 +110,15 @@ async function getIdeProcessInfoForUnix(): Promise<{
|
||||
} catch {
|
||||
// Ignore if getting grandparent fails, we'll just use the parent pid.
|
||||
}
|
||||
const { command } = await getProcessInfo(idePid);
|
||||
return { pid: idePid, command };
|
||||
const { command: ideCommand } = await getProcessInfo(idePid);
|
||||
return { pid: idePid, command: ideCommand };
|
||||
}
|
||||
|
||||
if (parentPid <= 1) {
|
||||
break; // Reached the root
|
||||
}
|
||||
currentPid = parentPid;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
// Process in chain died
|
||||
break;
|
||||
}
|
||||
@@ -124,10 +128,70 @@ async function getIdeProcessInfoForUnix(): Promise<{
|
||||
return { pid: currentPid, command };
|
||||
}
|
||||
|
||||
interface ProcessInfo {
|
||||
pid: number;
|
||||
parentPid: number;
|
||||
name: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
interface RawProcessInfo {
|
||||
ProcessId?: number;
|
||||
ParentProcessId?: number;
|
||||
Name?: string;
|
||||
CommandLine?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the IDE process info on Windows.
|
||||
* Fetches the entire process table on Windows.
|
||||
*/
|
||||
async function getProcessTableWindows(): Promise<Map<number, ProcessInfo>> {
|
||||
const processMap = new Map<number, ProcessInfo>();
|
||||
try {
|
||||
const powershellCommand =
|
||||
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress';
|
||||
const { stdout } = await execFileAsync(
|
||||
'powershell',
|
||||
['-NoProfile', '-NonInteractive', '-Command', 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the IDE process info on Windows using a snapshot approach.
|
||||
*
|
||||
* The strategy is to find the great-grandchild of the root process.
|
||||
* The strategy is to find the IDE process by looking for known IDE executables
|
||||
* in the process chain, with fallback to heuristics.
|
||||
*
|
||||
* @returns A promise that resolves to the PID and command of the IDE process.
|
||||
*/
|
||||
@@ -135,39 +199,131 @@ async function getIdeProcessInfoForWindows(): Promise<{
|
||||
pid: number;
|
||||
command: string;
|
||||
}> {
|
||||
let currentPid = process.pid;
|
||||
let previousPid = process.pid;
|
||||
// Fetch the entire process table in one go.
|
||||
const processMap = await getProcessTableWindows();
|
||||
|
||||
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
||||
try {
|
||||
const { parentPid } = await getProcessInfo(currentPid);
|
||||
const myPid = process.pid;
|
||||
const myProc = processMap.get(myPid);
|
||||
|
||||
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 (!myProc) {
|
||||
// Fallback: return current process info if snapshot fails
|
||||
return { pid: myPid, command: '' };
|
||||
}
|
||||
|
||||
// Known IDE process names (lowercase for case-insensitive comparison)
|
||||
const ideProcessNames = [
|
||||
'code.exe', // VS Code
|
||||
'code - insiders.exe', // VS Code Insiders
|
||||
'cursor.exe', // Cursor
|
||||
'windsurf.exe', // Windsurf
|
||||
'devenv.exe', // Visual Studio
|
||||
'rider64.exe', // JetBrains Rider
|
||||
'idea64.exe', // IntelliJ IDEA
|
||||
'pycharm64.exe', // PyCharm
|
||||
'webstorm64.exe', // WebStorm
|
||||
];
|
||||
|
||||
// Perform tree traversal in memory
|
||||
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)) {
|
||||
// Parent process not in map, stop traversal
|
||||
break;
|
||||
}
|
||||
curr = processMap.get(curr.parentPid);
|
||||
}
|
||||
|
||||
// Strategy 1: Look for known IDE process names in the chain
|
||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||
const proc = ancestors[i];
|
||||
const nameLower = proc.name.toLowerCase();
|
||||
|
||||
if (
|
||||
ideProcessNames.some((ideName) => nameLower === ideName.toLowerCase())
|
||||
) {
|
||||
return { pid: proc.pid, command: proc.command };
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Special handling for Git Bash (sh.exe/bash.exe) with missing parent
|
||||
// Check this first before general shell handling
|
||||
const gitBashNames = ['sh.exe', 'bash.exe'];
|
||||
const gitBashProc = ancestors.find((p) =>
|
||||
gitBashNames.some((name) => p.name.toLowerCase() === name.toLowerCase()),
|
||||
);
|
||||
|
||||
if (gitBashProc) {
|
||||
// Check if parent exists in process table
|
||||
const parentExists =
|
||||
gitBashProc.parentPid !== 0 && processMap.has(gitBashProc.parentPid);
|
||||
|
||||
if (!parentExists && gitBashProc.parentPid !== 0) {
|
||||
// Look for IDE processes in the entire process table
|
||||
const ideProcesses: ProcessInfo[] = [];
|
||||
for (const [, proc] of processMap) {
|
||||
const nameLower = proc.name.toLowerCase();
|
||||
if (
|
||||
ideProcessNames.some((ideName) => nameLower === ideName.toLowerCase())
|
||||
) {
|
||||
ideProcesses.push(proc);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentPid <= 0) {
|
||||
break; // Reached the root
|
||||
if (ideProcesses.length > 0) {
|
||||
// Prefer main process (without --type= parameter) over utility processes
|
||||
const mainProcesses = ideProcesses.filter(
|
||||
(p) => !p.command.includes('--type='),
|
||||
);
|
||||
const targetProcesses =
|
||||
mainProcesses.length > 0 ? mainProcesses : ideProcesses;
|
||||
|
||||
// Sort by PID and pick the one with lowest PID
|
||||
targetProcesses.sort((a, b) => a.pid - b.pid);
|
||||
return {
|
||||
pid: targetProcesses[0].pid,
|
||||
command: targetProcesses[0].command,
|
||||
};
|
||||
}
|
||||
} else if (parentExists) {
|
||||
// Git Bash parent exists, use it
|
||||
const gitBashIndex = ancestors.indexOf(gitBashProc);
|
||||
if (gitBashIndex >= 0 && gitBashIndex + 1 < ancestors.length) {
|
||||
const parentProc = ancestors[gitBashIndex + 1];
|
||||
return { pid: parentProc.pid, command: parentProc.command };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Look for other shell processes (cmd.exe, powershell.exe, etc.) and use their parent
|
||||
const otherShellNames = ['cmd.exe', 'powershell.exe', 'pwsh.exe'];
|
||||
for (let i = 0; i < ancestors.length; i++) {
|
||||
const proc = ancestors[i];
|
||||
const nameLower = proc.name.toLowerCase();
|
||||
|
||||
if (otherShellNames.some((shell) => nameLower === shell.toLowerCase())) {
|
||||
// The parent of the shell is likely closer to the IDE
|
||||
if (i + 1 < ancestors.length) {
|
||||
const parentProc = ancestors[i + 1];
|
||||
return { pid: parentProc.pid, command: parentProc.command };
|
||||
}
|
||||
previousPid = currentPid;
|
||||
currentPid = parentPid;
|
||||
} catch {
|
||||
// Process in chain died
|
||||
break;
|
||||
}
|
||||
}
|
||||
const { command } = await getProcessInfo(currentPid);
|
||||
return { pid: currentPid, command };
|
||||
|
||||
// Strategy 4: Use ancestors[length-3] as fallback (original logic)
|
||||
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 };
|
||||
}
|
||||
|
||||
return { pid: myPid, command: myProc.command };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user