Files
orca/src/shared/shell-process-readiness.ts
T
Brennan Benson 627716fdc1 perf(ports): stop leaving stuck lsof processes behind on a dead network mount (#27729)
* perf(ports): run lsof without blocking on dead network mounts

Without -b, lsof stat()s every mounted file system before answering. A
hard-mounted network share that stops responding blocks that call in the
kernel, so each port scan left an unkillable lsof behind. Pass -b -w to
the lsof queries that name no file (port list, process cwd, executable
path); their output is unchanged. The -U and +D queries keep stat
because -b makes lsof reject named-file arguments.

Linear: STA-9017, STA-9716

* perf(ports): name the stuck lsof helper in the comment; plain readiness test name

* test: query a known child executable in lsof readiness smoke test
2026-10-12 00:55:43 -07:00

112 lines
3.6 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'
import { LSOF_NONBLOCKING_ARGS } from './lsof-nonblocking-args'
import { PS_MAX_BUFFER_BYTES } from './process-table-snapshot'
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',
[...LSOF_NONBLOCKING_ARGS, '-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,
maxBuffer: PS_MAX_BUFFER_BYTES
})
])
const status = stdout.trim()
return status && executablePath
? { executablePath: await realpath(executablePath), foreground: status.includes('+') }
: null
}
function shellExecutableCandidates(
shellPath: string,
cwd: string,
pathEnv: string | undefined
): string[] {
return 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))
}
async function canonicalizeExecutable(candidate: string): Promise<string | null> {
try {
await access(candidate, constants.X_OK)
const canonicalPath = await realpath(candidate)
return (await stat(canonicalPath)).isFile() ? canonicalPath : null
} catch {
return null
}
}
export async function resolveShellExecutablePath(
shellPath: string,
cwd: string,
pathEnv: string | undefined
): Promise<string | null> {
for (const candidate of shellExecutableCandidates(shellPath, cwd, pathEnv)) {
const canonicalPath = await canonicalizeExecutable(candidate)
if (canonicalPath) {
return canonicalPath
}
}
return null
}
/** Every canonical executable `shellName` names on `pathEnv` — the installations a
* startup profile could legitimately `exec` into, and nothing a dropped-in binary
* outside the search path can reach. `shellName` must be a bare name. */
export async function resolveInstalledShellExecutablePaths(
shellName: string,
cwd: string,
pathEnv: string | undefined
): Promise<string[]> {
const canonicalPaths = await Promise.all(
shellExecutableCandidates(shellName, cwd, pathEnv).map(canonicalizeExecutable)
)
return [...new Set(canonicalPaths.filter((path): path is string => path !== null))]
}