2025-08-14 18:09:19 +00:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-08-25 22:11:27 +02:00
|
|
|
import { exec } from 'node:child_process';
|
|
|
|
|
import { promisify } from 'node:util';
|
|
|
|
|
import os from 'node:os';
|
|
|
|
|
import path from 'node:path';
|
2025-08-14 18:09:19 +00:00
|
|
|
|
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
|
|
2025-08-19 17:23:37 -07:00
|
|
|
const MAX_TRAVERSAL_DEPTH = 32;
|
|
|
|
|
|
2025-08-14 18:09:19 +00:00
|
|
|
/**
|
2025-08-25 11:39:57 -07:00
|
|
|
* Fetches the parent process ID, name, and command for a given process ID.
|
2025-08-14 18:09:19 +00:00
|
|
|
*
|
2025-08-19 17:23:37 -07:00
|
|
|
* @param pid The process ID to inspect.
|
2025-08-25 11:39:57 -07:00
|
|
|
* @returns A promise that resolves to the parent's PID, name, and command.
|
2025-08-14 18:09:19 +00:00
|
|
|
*/
|
2025-08-25 11:39:57 -07:00
|
|
|
async function getProcessInfo(pid: number): Promise<{
|
2025-08-19 17:23:37 -07:00
|
|
|
parentPid: number;
|
|
|
|
|
name: string;
|
2025-08-25 11:39:57 -07:00
|
|
|
command: string;
|
2025-08-19 17:23:37 -07:00
|
|
|
}> {
|
2025-08-14 18:09:19 +00:00
|
|
|
const platform = os.platform();
|
2025-08-19 17:23:37 -07:00
|
|
|
if (platform === 'win32') {
|
2025-08-25 11:39:57 -07:00
|
|
|
const command = `wmic process where "ProcessId=${pid}" get Name,ParentProcessId,CommandLine /value`;
|
2025-08-19 17:23:37 -07:00
|
|
|
const { stdout } = await execAsync(command);
|
|
|
|
|
const nameMatch = stdout.match(/Name=([^\n]*)/);
|
|
|
|
|
const processName = nameMatch ? nameMatch[1].trim() : '';
|
|
|
|
|
const ppidMatch = stdout.match(/ParentProcessId=(\d+)/);
|
|
|
|
|
const parentPid = ppidMatch ? parseInt(ppidMatch[1], 10) : 0;
|
2025-08-25 11:39:57 -07:00
|
|
|
const commandLineMatch = stdout.match(/CommandLine=([^\n]*)/);
|
|
|
|
|
const commandLine = commandLineMatch ? commandLineMatch[1].trim() : '';
|
|
|
|
|
return { parentPid, name: processName, command: commandLine };
|
2025-08-19 17:23:37 -07:00
|
|
|
} 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]);
|
2025-08-25 11:39:57 -07:00
|
|
|
return {
|
|
|
|
|
parentPid: isNaN(parentPid) ? 1 : parentPid,
|
|
|
|
|
name: processName,
|
|
|
|
|
command: fullCommand,
|
|
|
|
|
};
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-25 11:39:57 -07:00
|
|
|
* Finds the IDE process info on Unix-like systems.
|
2025-08-19 17:23:37 -07:00
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2025-08-25 11:39:57 -07:00
|
|
|
* @returns A promise that resolves to the PID and command of the IDE process.
|
2025-08-19 17:23:37 -07:00
|
|
|
*/
|
2025-08-25 11:39:57 -07:00
|
|
|
async function getIdeProcessInfoForUnix(): Promise<{
|
|
|
|
|
pid: number;
|
|
|
|
|
command: string;
|
|
|
|
|
}> {
|
2025-08-19 17:23:37 -07:00
|
|
|
const shells = ['zsh', 'bash', 'sh', 'tcsh', 'csh', 'ksh', 'fish', 'dash'];
|
2025-08-14 18:09:19 +00:00
|
|
|
let currentPid = process.pid;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
|
|
|
|
try {
|
2025-08-25 11:39:57 -07:00
|
|
|
const { parentPid, name } = await getProcessInfo(currentPid);
|
2025-08-19 17:23:37 -07:00
|
|
|
|
|
|
|
|
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.
|
2025-08-25 11:39:57 -07:00
|
|
|
let idePid = parentPid;
|
2025-08-19 17:23:37 -07:00
|
|
|
try {
|
2025-08-25 11:39:57 -07:00
|
|
|
const { parentPid: grandParentPid } = await getProcessInfo(parentPid);
|
2025-08-19 17:23:37 -07:00
|
|
|
if (grandParentPid > 1) {
|
2025-08-25 11:39:57 -07:00
|
|
|
idePid = grandParentPid;
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore if getting grandparent fails, we'll just use the parent pid.
|
|
|
|
|
}
|
2025-08-25 11:39:57 -07:00
|
|
|
const { command } = await getProcessInfo(idePid);
|
|
|
|
|
return { pid: idePid, command };
|
2025-08-14 18:09:19 +00:00
|
|
|
}
|
2025-08-19 17:23:37 -07:00
|
|
|
|
|
|
|
|
if (parentPid <= 1) {
|
|
|
|
|
break; // Reached the root
|
2025-08-14 18:09:19 +00:00
|
|
|
}
|
2025-08-19 17:23:37 -07:00
|
|
|
currentPid = parentPid;
|
|
|
|
|
} catch {
|
|
|
|
|
// Process in chain died
|
2025-08-14 18:09:19 +00:00
|
|
|
break;
|
|
|
|
|
}
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
2025-08-14 18:09:19 +00:00
|
|
|
|
2025-08-25 11:39:57 -07:00
|
|
|
const { command } = await getProcessInfo(currentPid);
|
|
|
|
|
return { pid: currentPid, command };
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
2025-08-14 18:09:19 +00:00
|
|
|
|
2025-08-19 17:23:37 -07:00
|
|
|
/**
|
2025-08-25 11:39:57 -07:00
|
|
|
* Finds the IDE process info on Windows.
|
2025-08-19 17:23:37 -07:00
|
|
|
*
|
2025-08-25 11:39:57 -07:00
|
|
|
* The strategy is to find the great-grandchild of the root process.
|
2025-08-19 17:23:37 -07:00
|
|
|
*
|
2025-08-25 11:39:57 -07:00
|
|
|
* @returns A promise that resolves to the PID and command of the IDE process.
|
2025-08-19 17:23:37 -07:00
|
|
|
*/
|
2025-08-25 11:39:57 -07:00
|
|
|
async function getIdeProcessInfoForWindows(): Promise<{
|
|
|
|
|
pid: number;
|
|
|
|
|
command: string;
|
|
|
|
|
}> {
|
2025-08-19 17:23:37 -07:00
|
|
|
let currentPid = process.pid;
|
2025-08-25 11:39:57 -07:00
|
|
|
let previousPid = process.pid;
|
2025-08-19 17:23:37 -07:00
|
|
|
|
|
|
|
|
for (let i = 0; i < MAX_TRAVERSAL_DEPTH; i++) {
|
|
|
|
|
try {
|
2025-08-25 11:39:57 -07:00
|
|
|
const { parentPid } = await getProcessInfo(currentPid);
|
2025-08-19 17:23:37 -07:00
|
|
|
|
|
|
|
|
if (parentPid > 0) {
|
|
|
|
|
try {
|
2025-08-25 11:39:57 -07:00
|
|
|
const { parentPid: grandParentPid } = await getProcessInfo(parentPid);
|
2025-08-19 17:23:37 -07:00
|
|
|
if (grandParentPid === 0) {
|
2025-08-25 11:39:57 -07:00
|
|
|
// 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 };
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// getting grandparent failed, proceed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parentPid <= 0) {
|
|
|
|
|
break; // Reached the root
|
|
|
|
|
}
|
2025-08-25 11:39:57 -07:00
|
|
|
previousPid = currentPid;
|
2025-08-19 17:23:37 -07:00
|
|
|
currentPid = parentPid;
|
|
|
|
|
} catch {
|
|
|
|
|
// Process in chain died
|
2025-08-14 18:09:19 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 11:39:57 -07:00
|
|
|
const { command } = await getProcessInfo(currentPid);
|
|
|
|
|
return { pid: currentPid, command };
|
2025-08-14 18:09:19 +00:00
|
|
|
}
|
2025-08-19 17:23:37 -07:00
|
|
|
|
|
|
|
|
/**
|
2025-08-25 11:39:57 -07:00
|
|
|
* Traverses up the process tree to find the process ID and command of the IDE.
|
2025-08-19 17:23:37 -07:00
|
|
|
*
|
|
|
|
|
* 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
|
2025-08-25 11:39:57 -07:00
|
|
|
* top-level ancestor process ID and command as a fallback.
|
2025-08-19 17:23:37 -07:00
|
|
|
*
|
2025-08-25 11:39:57 -07:00
|
|
|
* @returns A promise that resolves to the PID and command of the IDE process.
|
2025-08-19 17:23:37 -07:00
|
|
|
* @throws Will throw an error if the underlying shell commands fail.
|
|
|
|
|
*/
|
2025-08-25 11:39:57 -07:00
|
|
|
export async function getIdeProcessInfo(): Promise<{
|
|
|
|
|
pid: number;
|
|
|
|
|
command: string;
|
|
|
|
|
}> {
|
2025-08-19 17:23:37 -07:00
|
|
|
const platform = os.platform();
|
|
|
|
|
|
|
|
|
|
if (platform === 'win32') {
|
2025-08-25 11:39:57 -07:00
|
|
|
return getIdeProcessInfoForWindows();
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-25 11:39:57 -07:00
|
|
|
return getIdeProcessInfoForUnix();
|
2025-08-19 17:23:37 -07:00
|
|
|
}
|