openclaw/src/shared/pid-alive.ts

71 lines
2.0 KiB
TypeScript

import fsSync from "node:fs";
function isValidPid(pid: number): boolean {
return Number.isInteger(pid) && pid > 0;
}
/**
* Check if a process is a zombie on Linux by reading /proc/<pid>/status.
* Returns false on non-Linux platforms or if the proc file can't be read.
*/
function isZombieProcess(pid: number): boolean {
if (process.platform !== "linux") {
return false;
}
try {
const status = fsSync.readFileSync(`/proc/${pid}/status`, "utf8");
const stateMatch = status.match(/^State:\s+(\S)/m);
return stateMatch?.[1] === "Z";
} catch {
return false;
}
}
export function isPidAlive(pid: number): boolean {
if (!isValidPid(pid)) {
return false;
}
try {
process.kill(pid, 0);
} catch {
return false;
}
if (isZombieProcess(pid)) {
return false;
}
return true;
}
/**
* Read the process start time (field 22 "starttime") from /proc/<pid>/stat.
* Returns the value in clock ticks since system boot, or null on non-Linux
* platforms or if the proc file can't be read.
*
* This is used to detect PID recycling: if two readings for the same PID
* return different starttimes, the PID has been reused by a different process.
*/
export function getProcessStartTime(pid: number): number | null {
if (process.platform !== "linux") {
return null;
}
if (!isValidPid(pid)) {
return null;
}
try {
const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
const commEndIndex = stat.lastIndexOf(")");
if (commEndIndex < 0) {
return null;
}
// The comm field (field 2) is wrapped in parens and can contain spaces,
// so split after the last ")" to get fields 3..N reliably.
const afterComm = stat.slice(commEndIndex + 1).trimStart();
const fields = afterComm.split(/\s+/);
// field 22 (starttime) = index 19 after the comm-split (field 3 is index 0).
const starttime = Number(fields[19]);
return Number.isInteger(starttime) && starttime >= 0 ? starttime : null;
} catch {
return null;
}
}