import { execFileSync } from "node:child_process"; import * as fs from "node:fs"; export type ProcessLiveness = "alive" | "dead" | "unknown"; type KillFn = (pid: number, signal: 0) => unknown; /** * `kill(pid, 0)` probe. ESRCH proves the pid is gone, EPERM proves it exists (owned by someone else), * anything else proves nothing. A live pid may still be a *reused* pid; pair with `processStartKey`. */ export function processLiveness(pid: number, kill: KillFn = process.kill): ProcessLiveness { if (!Number.isSafeInteger(pid) || pid <= 0) return "unknown"; try { kill(pid, 0); return "alive"; } catch (error) { const code = typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined; if (code === "ESRCH") return "dead"; if (code === "EPERM") return "alive"; return "unknown"; } } export function linuxProcessStartKey(pid: number): string | undefined { try { const raw = fs.readFileSync(`/proc/${pid}/stat`, "utf-8"); const tail = raw.slice(raw.lastIndexOf(")") + 2).trim().split(/\s+/); return tail[19] ? `linux:${tail[19]}` : undefined; } catch { return undefined; } } export function psProcessStartKey(pid: number): string | undefined { try { const raw = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000 }).trim(); return raw ? `ps:${raw}` : undefined; } catch { return undefined; } } export function windowsProcessStartKey(pid: number): string | undefined { try { const raw = execFileSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CreationDate`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000, windowsHide: true }).trim(); return raw ? `win:${raw}` : undefined; } catch { return undefined; } } type StartKeyResolvers = { linux?: (pid: number) => string | undefined; ps?: (pid: number) => string | undefined; windows?: (pid: number) => string | undefined; }; /** * Resolves a start key without caching. Linux prefers `/proc` and falls back to `ps`; Windows uses CIM; every * other unix (macOS, BSDs) uses `ps -o lstart=`. `undefined` means the identity could not be established. */ export function computeProcessStartKey(pid: number, platform: NodeJS.Platform = process.platform, resolvers: StartKeyResolvers = {}): string | undefined { const linux = resolvers.linux ?? linuxProcessStartKey; const ps = resolvers.ps ?? psProcessStartKey; const windows = resolvers.windows ?? windowsProcessStartKey; if (platform === "linux") return linux(pid) ?? ps(pid); if (platform === "win32") return windows(pid); return ps(pid); } let currentProcessKey: string | undefined | null = null; export function processStartKey(pid: number): string | undefined { // Foreign PIDs can be reused while this process lives, so resolve them afresh. if (pid !== process.pid) return computeProcessStartKey(pid); if (currentProcessKey === null) currentProcessKey = computeProcessStartKey(pid); return currentProcessKey; } /** Start keys are only comparable within one scheme (`linux:`, `ps:`, `win:`). */ export function startKeyScheme(key: string): string { const index = key.indexOf(":"); return index < 0 ? "" : key.slice(0, index); }