/ src / utils / genericProcessUtils.ts
genericProcessUtils.ts
  1  import {
  2    execFileNoThrowWithCwd,
  3    execSyncWithDefaults_DEPRECATED,
  4  } from './execFileNoThrow.js'
  5  
  6  // This file contains platform-agnostic implementations of common `ps` type commands.
  7  // When adding new code to this file, make sure to handle:
  8  // - Win32, as `ps` within cygwin and WSL may not behave as expected, particularly when attempting to access processes on the host.
  9  // - Unix vs BSD-style `ps` have different options.
 10  
 11  /**
 12   * Check if a process with the given PID is running (signal 0 probe).
 13   *
 14   * PID ≤ 1 returns false (0 is current process group, 1 is init).
 15   *
 16   * Note: `process.kill(pid, 0)` throws EPERM when the process exists but is
 17   * owned by another user. This reports such processes as NOT running, which
 18   * is conservative for lock recovery (we won't steal a live lock).
 19   */
 20  export function isProcessRunning(pid: number): boolean {
 21    if (pid <= 1) return false
 22    try {
 23      process.kill(pid, 0)
 24      return true
 25    } catch {
 26      return false
 27    }
 28  }
 29  
 30  /**
 31   * Gets the ancestor process chain for a given process (up to maxDepth levels)
 32   * @param pid - The starting process ID
 33   * @param maxDepth - Maximum number of ancestors to fetch (default: 10)
 34   * @returns Array of ancestor PIDs from immediate parent to furthest ancestor
 35   */
 36  export async function getAncestorPidsAsync(
 37    pid: string | number,
 38    maxDepth = 10,
 39  ): Promise<number[]> {
 40    if (process.platform === 'win32') {
 41      // For Windows, use a PowerShell script that walks the process tree
 42      const script = `
 43        $pid = ${String(pid)}
 44        $ancestors = @()
 45        for ($i = 0; $i -lt ${maxDepth}; $i++) {
 46          $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue
 47          if (-not $proc -or -not $proc.ParentProcessId -or $proc.ParentProcessId -eq 0) { break }
 48          $pid = $proc.ParentProcessId
 49          $ancestors += $pid
 50        }
 51        $ancestors -join ','
 52      `.trim()
 53  
 54      const result = await execFileNoThrowWithCwd(
 55        'powershell.exe',
 56        ['-NoProfile', '-Command', script],
 57        { timeout: 3000 },
 58      )
 59      if (result.code !== 0 || !result.stdout?.trim()) {
 60        return []
 61      }
 62      return result.stdout
 63        .trim()
 64        .split(',')
 65        .filter(Boolean)
 66        .map(p => parseInt(p, 10))
 67        .filter(p => !isNaN(p))
 68    }
 69  
 70    // For Unix, use a shell command that walks up the process tree
 71    // This uses a single process invocation instead of multiple sequential calls
 72    const script = `pid=${String(pid)}; for i in $(seq 1 ${maxDepth}); do ppid=$(ps -o ppid= -p $pid 2>/dev/null | tr -d ' '); if [ -z "$ppid" ] || [ "$ppid" = "0" ] || [ "$ppid" = "1" ]; then break; fi; echo $ppid; pid=$ppid; done`
 73  
 74    const result = await execFileNoThrowWithCwd('sh', ['-c', script], {
 75      timeout: 3000,
 76    })
 77    if (result.code !== 0 || !result.stdout?.trim()) {
 78      return []
 79    }
 80    return result.stdout
 81      .trim()
 82      .split('\n')
 83      .filter(Boolean)
 84      .map(p => parseInt(p, 10))
 85      .filter(p => !isNaN(p))
 86  }
 87  
 88  /**
 89   * Gets the command line for a given process
 90   * @param pid - The process ID to get the command for
 91   * @returns The command line string, or null if not found
 92   * @deprecated Use getAncestorCommandsAsync instead
 93   */
 94  export function getProcessCommand(pid: string | number): string | null {
 95    try {
 96      const pidStr = String(pid)
 97      const command =
 98        process.platform === 'win32'
 99          ? `powershell.exe -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ProcessId=${pidStr}\\").CommandLine"`
100          : `ps -o command= -p ${pidStr}`
101  
102      const result = execSyncWithDefaults_DEPRECATED(command, { timeout: 1000 })
103      return result ? result.trim() : null
104    } catch {
105      return null
106    }
107  }
108  
109  /**
110   * Gets the command lines for a process and its ancestors in a single call
111   * @param pid - The starting process ID
112   * @param maxDepth - Maximum depth to traverse (default: 10)
113   * @returns Array of command strings for the process chain
114   */
115  export async function getAncestorCommandsAsync(
116    pid: string | number,
117    maxDepth = 10,
118  ): Promise<string[]> {
119    if (process.platform === 'win32') {
120      // For Windows, use a PowerShell script that walks the process tree and collects commands
121      const script = `
122        $currentPid = ${String(pid)}
123        $commands = @()
124        for ($i = 0; $i -lt ${maxDepth}; $i++) {
125          $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$currentPid" -ErrorAction SilentlyContinue
126          if (-not $proc) { break }
127          if ($proc.CommandLine) { $commands += $proc.CommandLine }
128          if (-not $proc.ParentProcessId -or $proc.ParentProcessId -eq 0) { break }
129          $currentPid = $proc.ParentProcessId
130        }
131        $commands -join [char]0
132      `.trim()
133  
134      const result = await execFileNoThrowWithCwd(
135        'powershell.exe',
136        ['-NoProfile', '-Command', script],
137        { timeout: 3000 },
138      )
139      if (result.code !== 0 || !result.stdout?.trim()) {
140        return []
141      }
142      return result.stdout.split('\0').filter(Boolean)
143    }
144  
145    // For Unix, use a shell command that walks up the process tree and collects commands
146    // Using null byte as separator to handle commands with newlines
147    const script = `currentpid=${String(pid)}; for i in $(seq 1 ${maxDepth}); do cmd=$(ps -o command= -p $currentpid 2>/dev/null); if [ -n "$cmd" ]; then printf '%s\\0' "$cmd"; fi; ppid=$(ps -o ppid= -p $currentpid 2>/dev/null | tr -d ' '); if [ -z "$ppid" ] || [ "$ppid" = "0" ] || [ "$ppid" = "1" ]; then break; fi; currentpid=$ppid; done`
148  
149    const result = await execFileNoThrowWithCwd('sh', ['-c', script], {
150      timeout: 3000,
151    })
152    if (result.code !== 0 || !result.stdout?.trim()) {
153      return []
154    }
155    return result.stdout.split('\0').filter(Boolean)
156  }
157  
158  /**
159   * Gets the child process IDs for a given process
160   * @param pid - The parent process ID
161   * @returns Array of child process IDs as numbers
162   */
163  export function getChildPids(pid: string | number): number[] {
164    try {
165      const pidStr = String(pid)
166      const command =
167        process.platform === 'win32'
168          ? `powershell.exe -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pidStr}\\").ProcessId"`
169          : `pgrep -P ${pidStr}`
170  
171      const result = execSyncWithDefaults_DEPRECATED(command, { timeout: 1000 })
172      if (!result) {
173        return []
174      }
175      return result
176        .trim()
177        .split('\n')
178        .filter(Boolean)
179        .map(p => parseInt(p, 10))
180        .filter(p => !isNaN(p))
181    } catch {
182      return []
183    }
184  }