diff --git a/packages/core/src/ide/process-utils.test.ts b/packages/core/src/ide/process-utils.test.ts index e6c68f1449..9176dad49e 100644 --- a/packages/core/src/ide/process-utils.test.ts +++ b/packages/core/src/ide/process-utils.test.ts @@ -65,132 +65,118 @@ 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)); - } - return Promise.reject(new Error('Invalid command for mock')); - }); + // process (1000) -> powershell (900) -> code (800) -> wininit (700) -> root (0) + // Ancestors: [1000, 900, 800, 700] + // Target (great-grandchild of root): 900 + 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.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); const result = await getIdeProcessInfo(); 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'); - mockedExec - .mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue - .mockResolvedValueOnce({ - stdout: - '{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}', - }); // Fallback call + // process (1000) -> root (0) + const processes = [ + { + ProcessId: 1000, + ParentProcessId: 0, + Name: 'node.exe', + CommandLine: 'node.exe', + }, + ]; + mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); 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 () => { (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.mockResolvedValueOnce({ stdout: '{"invalid":json}' }); + // Fallback to getProcessInfo for current PID + mockedExec.mockResolvedValueOnce({ stdout: '' }); 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'); - 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 process = { + ProcessId: 1000, + ParentProcessId: 0, + Name: 'node.exe', + CommandLine: 'node.exe', + }; + mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(process) }); 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, - }); + expect(result).toEqual({ pid: 1000, command: 'node.exe' }); }); - 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'); - mockedExec - .mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0 - .mockResolvedValueOnce({ - stdout: - '{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}', - }); // Get grandparent info + // process (1000) -> parent (900) -> missing (800) + const processes = [ + { + ProcessId: 1000, + ParentProcessId: 900, + 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(); - expect(result).toEqual({ pid: 1000, command: 'root.exe' }); + // Ancestors: [1000, 900]. Length < 3, returns last (900) + expect(result).toEqual({ pid: 900, command: 'parent.exe' }); }); }); }); diff --git a/packages/core/src/ide/process-utils.ts b/packages/core/src/ide/process-utils.ts index 170b1df187..5c1ca570a6 100644 --- a/packages/core/src/ide/process-utils.ts +++ b/packages/core/src/ide/process-utils.ts @@ -13,8 +13,67 @@ const execAsync = promisify(exec); 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> { + const processMap = new Map(); + 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. * @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; }> { 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 { stdout } = await execAsync(command); - const trimmedStdout = stdout.trim(); - if (!trimmedStdout) { - return { parentPid: 0, name: '', command: '' }; - } - const ppidString = trimmedStdout.split(/\s+/)[0]; - const parentPid = parseInt(ppidString, 10); - const fullCommand = trimmedStdout.substring(ppidString.length).trim(); - const processName = path.basename(fullCommand.split(' ')[0]); - return { - parentPid: isNaN(parentPid) ? 1 : parentPid, - name: processName, - command: fullCommand, - }; + const command = `ps -o ppid=,command= -p ${pid}`; + const { stdout } = await execAsync(command); + const trimmedStdout = stdout.trim(); + if (!trimmedStdout) { + return { parentPid: 0, name: '', command: '' }; } + const parts = trimmedStdout.split(/\s+/); + const ppidString = parts[0]; + const parentPid = parseInt(ppidString, 10); + const fullCommand = trimmedStdout.substring(ppidString.length).trim(); + const processName = path.basename(fullCommand.split(' ')[0]); + + return { + parentPid: isNaN(parentPid) ? 1 : parentPid, + name: processName, + command: fullCommand, + }; } catch (_e) { - console.debug(`Failed to get process info for pid ${pid}:`, _e); return { parentPid: 0, name: '', command: '' }; } } @@ -125,49 +159,45 @@ async function getIdeProcessInfoForUnix(): Promise<{ } /** - * Finds the IDE process info on Windows. - * - * 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. + * Finds the IDE process info on Windows using a snapshot approach. */ 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(); + const myPid = process.pid; + const myProc = processMap.get(myPid); - for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) { - try { - const { parentPid } = await getProcessInfo(currentPid); - - 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) { - break; // Reached the root - } - previousPid = currentPid; - currentPid = parentPid; - } catch { - // Process in chain died - break; - } + if (!myProc) { + // Fallback: try to get info for current process directly if snapshot fails + const { command } = await getProcessInfo(myPid); + return { pid: myPid, command }; } - const { command } = await getProcessInfo(currentPid); - return { pid: currentPid, command }; + + // Perform tree traversal in memory. + // 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 + } + curr = processMap.get(curr.parentPid); + } + + 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 }; } /**