2025-08-14 18:09:19 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import { exec } from 'child_process' ;
import { promisify } from 'util' ;
import os from 'os' ;
2025-08-19 17:23:37 -07:00
import path from '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-19 17:23:37 -07:00
console . error (
'Failed to find shell process in the process tree. Falling back to top-level process, which may be inaccurate. If you see this, please file a bug via /bug.' ,
) ;
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
}