import { execFile } from "node:child_process"; import { readFile, readlink } from "node:fs/promises"; import { hostname } from "node:os"; import { isAbsolute, normalize } from "node:path"; import { promisify } from "node:util"; import type { ProcessIdentity, ProcessIdentityInspector, ProcessInspection, } from "./workflow-lease.js"; const execFileAsync = promisify(execFile); /** Reads PID-reuse-resistant identity evidence without treating age or timeout as death proof. */ export class SystemProcessIdentityInspector implements ProcessIdentityInspector { async current(): Promise { const inspection = await this.inspect(process.pid); if (inspection.kind !== "live") { throw new Error( inspection.kind === "unknown" ? `Current process identity is unavailable: ${inspection.diagnostic}` : "Current process disappeared during identity inspection", ); } return inspection.identity; } async inspect(pid: number): Promise { if (!Number.isSafeInteger(pid) || pid <= 0) { return { kind: "unknown", diagnostic: "PID must be a positive integer" }; } if (process.platform === "linux") return inspectLinux(pid); if (process.platform === "darwin") return inspectDarwin(pid); return { kind: "unknown", diagnostic: `Process start identity is unsupported on ${process.platform}`, }; } } async function inspectLinux(pid: number): Promise { try { const [stat, executable, bootIdentity] = await Promise.all([ readFile(`/proc/${pid}/stat`, "utf8"), readlink(`/proc/${pid}/exe`), readFile("/proc/sys/kernel/random/boot_id", "utf8"), ]); const closing = stat.lastIndexOf(")"); if (closing < 0) return { kind: "unknown", diagnostic: "Linux process stat is malformed" }; const fields = stat.slice(closing + 2).trim().split(/\s+/); const startTicks = fields[19]; if (!startTicks) return { kind: "unknown", diagnostic: "Linux process start ticks are missing" }; return { kind: "live", identity: { pid, processStartIdentity: `linux-start-ticks:${startTicks}`, bootIdentity: bootIdentity.trim() || null, executable: normalize(executable), hostname: hostname(), }, }; } catch (error) { return classifyInspectionFailure(error, pid); } } async function inspectDarwin(pid: number): Promise { try { const [{ stdout }, boot] = await Promise.all([ execFileAsync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "comm="], { encoding: "utf8", maxBuffer: 8_192, timeout: 2_000, }), execFileAsync("sysctl", ["-n", "kern.boottime"], { encoding: "utf8", maxBuffer: 8_192, timeout: 2_000, }), ]); const line = stdout.trim(); if (line.length === 0) return { kind: "absent" }; const start = line.slice(0, 24).trim(); const psExecutable = line.slice(24).trim(); if (start.length === 0) { return { kind: "unknown", diagnostic: "Darwin ps start identity is incomplete" }; } const selectedExecutable = selectDarwinExecutable( psExecutable, pid === process.pid ? process.execPath : null, ); const executable = selectedExecutable === null ? await inspectDarwinTextExecutable(pid) : { kind: "resolved" as const, path: selectedExecutable }; if (executable.kind === "unknown") { return executable; } return { kind: "live", identity: { pid, processStartIdentity: `darwin-lstart:${start}`, bootIdentity: boot.stdout.trim() || null, executable: executable.path, hostname: hostname(), }, }; } catch (error) { return classifyInspectionFailure(error, pid); } } /** Uses current-process evidence only when ps reports a relative argv[0]. */ export function selectDarwinExecutable( psExecutable: string, currentExecutable: string | null, ): string | null { const reported = psExecutable.trim(); if (isAbsolute(reported)) return normalize(reported); if (currentExecutable !== null && isAbsolute(currentExecutable)) { return normalize(currentExecutable); } return null; } /** Reads the process text mapping when ps only exposes a mutable process title. */ async function inspectDarwinTextExecutable( pid: number, ): Promise< | { readonly kind: "resolved"; readonly path: string } | { readonly kind: "unknown"; readonly diagnostic: string } > { try { const { stdout } = await execFileAsync( "/usr/sbin/lsof", ["-a", "-p", String(pid), "-d", "txt", "-Fn"], { encoding: "utf8", maxBuffer: 262_144, timeout: 2_000, killSignal: "SIGKILL", }, ); const lines = stdout.split(/\r?\n/); for (let index = 0; index < lines.length - 1; index += 1) { if (lines[index] !== "ftxt") continue; const name = lines[index + 1] ?? ""; const path = name.startsWith("n") ? name.slice(1) : ""; if (isAbsolute(path)) return { kind: "resolved", path: normalize(path) }; } return { kind: "unknown", diagnostic: "Darwin process executable is unavailable" }; } catch (error) { return { kind: "unknown", diagnostic: `Could not inspect Darwin process executable for PID ${pid}: ${ error instanceof Error ? error.message : String(error) }`, }; } } function classifyInspectionFailure(error: unknown, pid: number): ProcessInspection { if (isNodeError(error) && (error.code === "ENOENT" || error.code === "ESRCH")) { return { kind: "absent" }; } if (isExecFailure(error) && error.code === 1) return { kind: "absent" }; return { kind: "unknown", diagnostic: `Could not inspect PID ${pid}: ${error instanceof Error ? error.message : String(error)}`, }; } function isNodeError(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && "code" in error; } function isExecFailure(error: unknown): error is Error & { readonly code: number } { return error instanceof Error && "code" in error && typeof error.code === "number"; }