import { execFile as execFileCallback } from 'node:child_process' import { constants } from 'node:fs' import { access, readlink, realpath, stat } from 'node:fs/promises' import { delimiter, isAbsolute, resolve } from 'node:path' import { promisify } from 'node:util' const execFile = promisify(execFileCallback) const PROCESS_READINESS_TIMEOUT_MS = 3000 const DEFAULT_POSIX_EXEC_PATH = '/usr/bin:/bin' export type ShellProcessReadiness = { executablePath: string foreground: boolean } export function parseDarwinExecutablePath(stdout: string): string | null { const lines = stdout.split(/\r?\n/) const textIndex = lines.indexOf('ftxt') const pathLine = textIndex === -1 ? undefined : lines[textIndex + 1] return pathLine?.startsWith('n') ? pathLine.slice(1) : null } async function readExecutablePath(pid: number): Promise { if (process.platform === 'linux') { return readlink(`/proc/${pid}/exe`) } if (process.platform !== 'darwin') { return null } const { stdout } = await execFile( '/usr/sbin/lsof', ['-a', '-p', String(pid), '-d', 'txt', '-Fn'], { encoding: 'utf8', timeout: PROCESS_READINESS_TIMEOUT_MS } ) return parseDarwinExecutablePath(stdout) } export async function readShellProcessReadiness( pid: number ): Promise { const [executablePath, { stdout }] = await Promise.all([ readExecutablePath(pid), execFile('ps', ['-p', String(pid), '-o', 'stat='], { encoding: 'utf8', timeout: PROCESS_READINESS_TIMEOUT_MS }) ]) const status = stdout.trim() return status && executablePath ? { executablePath: await realpath(executablePath), foreground: status.includes('+') } : null } export async function resolveShellExecutablePath( shellPath: string, cwd: string, pathEnv: string | undefined ): Promise { const candidates = shellPath.includes('/') ? [isAbsolute(shellPath) ? shellPath : resolve(cwd, shellPath)] : ( pathEnv ?? (process.platform === 'win32' ? (process.env.PATH ?? '') : DEFAULT_POSIX_EXEC_PATH) ) .split(delimiter) .map((entry) => resolve(isAbsolute(entry) ? entry : resolve(cwd, entry), shellPath)) for (const candidate of candidates) { try { await access(candidate, constants.X_OK) const canonicalPath = await realpath(candidate) if ((await stat(canonicalPath)).isFile()) { return canonicalPath } } catch {} } return null }