/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import os from 'node:os'; import path from 'node:path'; const execAsync = promisify(exec); const MAX_TRAVERSAL_DEPTH = 32; /** * Fetches the parent process ID, name, and command for a given process ID. * * @param pid The process ID to inspect. * @returns A promise that resolves to the parent's PID, name, and command. */ async function getProcessInfo(pid: number): Promise<{ parentPid: number; name: string; 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(); 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, }; } } catch (_e) { console.debug(`Failed to get process info for pid ${pid}:`, _e); return { parentPid: 0, name: '', command: '' }; } } /** * Finds the IDE process info on Unix-like systems. * * The strategy is to find the shell process that spawned the CLI, and then * find that shell's parent process (the IDE). To get the true IDE process, * we traverse one level higher to get the grandparent. * * @returns A promise that resolves to the PID and command of the IDE process. */ async function getIdeProcessInfoForUnix(): Promise<{ pid: number; command: string; }> { const shells = ['zsh', 'bash', 'sh', 'tcsh', 'csh', 'ksh', 'fish', 'dash']; let currentPid = process.pid; for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) { try { const { parentPid, name } = await getProcessInfo(currentPid); const isShell = shells.some((shell) => name === shell); if (isShell) { // The direct parent of the shell is often a utility process (e.g. VS // Code's `ptyhost` process). To get the true IDE process, we need to // traverse one level higher to get the grandparent. let idePid = parentPid; try { const { parentPid: grandParentPid } = await getProcessInfo(parentPid); if (grandParentPid > 1) { idePid = grandParentPid; } } catch { // Ignore if getting grandparent fails, we'll just use the parent pid. } const { command } = await getProcessInfo(idePid); return { pid: idePid, command }; } if (parentPid <= 1) { break; // Reached the root } currentPid = parentPid; } catch { // Process in chain died break; } } const { command } = await getProcessInfo(currentPid); return { pid: currentPid, command }; } /** * 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. */ async function getIdeProcessInfoForWindows(): Promise<{ pid: number; command: string; }> { let currentPid = process.pid; let previousPid = process.pid; 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; } } const { command } = await getProcessInfo(currentPid); return { pid: currentPid, command }; } /** * Traverses up the process tree to find the process ID and command of the IDE. * * This function uses different strategies depending on the operating system * to identify the main application process (e.g., the main VS Code window * process). * * If the IDE process cannot be reliably identified, it will return the * top-level ancestor process ID and command as a fallback. * * @returns A promise that resolves to the PID and command of the IDE process. */ export async function getIdeProcessInfo(): Promise<{ pid: number; command: string; }> { const platform = os.platform(); if (platform === 'win32') { return getIdeProcessInfoForWindows(); } return getIdeProcessInfoForUnix(); }