Files
orca/src/shared/shell-process-readiness.ts
T

81 lines
2.5 KiB
TypeScript

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<string | null> {
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<ShellProcessReadiness | null> {
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<string | null> {
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
}