← Files Codex Process JobsARCHIVED FILE
scripts/process-control.mjs
4.99 KB · Sep 30, 2026 · 23:13 UTC
import fs from "node:fs";
import { spawnSync } from "node:child_process";
export function parseLinuxProcStat(contents) {
const text = String(contents);
const closeParen = text.lastIndexOf(")");
if (closeParen < 0) throw new Error("Malformed /proc stat: missing process name terminator");
const fieldsAfterName = text.slice(closeParen + 2).trim().split(/\s+/);
const startTime = fieldsAfterName[19];
if (!startTime) throw new Error("Malformed /proc stat: missing starttime field");
return startTime;
}
export function getProcessIdentity(pid, options = {}) {
if (!Number.isInteger(pid) || pid <= 0) throw new Error(`Invalid pid: ${pid}`);
const platform = options.platform ?? process.platform;
if (platform === "linux") {
const readFileSync = options.readFileSync ?? fs.readFileSync;
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
return `linux:${parseLinuxProcStat(stat)}`;
}
if (platform === "darwin") {
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
const result = spawnSyncImpl("ps", ["-o", "lstart=,comm=", "-p", String(pid)], {
encoding: "utf8",
shell: false,
});
if (result.error) throw result.error;
if (result.status !== 0 || !result.stdout?.trim()) {
throw new Error(`Unable to read identity for process ${pid}`);
}
return `darwin:${result.stdout.trim()}`;
}
throw new Error(`Unsupported platform: ${platform}. Use macOS or Linux.`);
}
export function validateProcessIdentity(pid, expectedIdentity, options = {}) {
if (!expectedIdentity) return false;
try {
return getProcessIdentity(pid, options) === expectedIdentity;
} catch {
return false;
}
}
export function isProcessAlive(pid, killImpl = process.kill.bind(process)) {
try {
killImpl(pid, 0);
return true;
} catch (error) {
if (error?.code === "EPERM") return true;
return false;
}
}
function signalProcessGroup(pid, signal, killImpl) {
try {
killImpl(-pid, signal);
return true;
} catch (error) {
if (error?.code === "ESRCH") return false;
throw error;
}
}
function isProcessGroupAlive(pid, killImpl) {
try {
killImpl(-pid, 0);
return true;
} catch (error) {
if (error?.code === "EPERM") return true;
if (error?.code === "ESRCH") return false;
throw error;
}
}
function isTrackedProcessGroupAlive(pid, expectedIdentity, options) {
const { validateIdentity, killImpl, identityOptions } = options;
if (validateIdentity(pid, expectedIdentity, identityOptions)) return true;
// POSIX reserves a process-group leader's numeric PID for the entire group
// lifetime. If that PID is occupied by a different process, the validated
// group has ended and must not be signalled. If the PID is free but the PGID
// still exists, leaderless descendants are keeping the original group alive.
if (isProcessAlive(pid, killImpl)) return false;
return isProcessGroupAlive(pid, killImpl);
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function terminateTrackedProcess(pid, expectedIdentity, options = {}) {
const validateIdentity = options.validateIdentity ?? validateProcessIdentity;
const killImpl = options.killImpl ?? process.kill.bind(process);
const graceMs = options.graceMs ?? 5_000;
const pollMs = options.pollMs ?? 100;
if (!validateIdentity(pid, expectedIdentity, options.identityOptions)) {
return { terminated: false, reason: "process identity mismatch" };
}
const termDelivered = signalProcessGroup(pid, "SIGTERM", killImpl);
if (!termDelivered) return { terminated: true, forced: false, alreadyExited: true };
const deadline = Date.now() + graceMs;
while (Date.now() < deadline) {
if (!isTrackedProcessGroupAlive(pid, expectedIdentity, {
validateIdentity,
killImpl,
identityOptions: options.identityOptions,
})) {
return { terminated: true, forced: false };
}
await delay(pollMs);
}
if (!isTrackedProcessGroupAlive(pid, expectedIdentity, {
validateIdentity,
killImpl,
identityOptions: options.identityOptions,
})) {
return { terminated: true, forced: false };
}
const killDelivered = signalProcessGroup(pid, "SIGKILL", killImpl);
const killDeadline = Date.now() + Math.min(2_000, Math.max(250, graceMs));
while (Date.now() < killDeadline) {
if (!isTrackedProcessGroupAlive(pid, expectedIdentity, {
validateIdentity,
killImpl,
identityOptions: options.identityOptions,
})) {
return { terminated: true, forced: killDelivered };
}
await delay(pollMs);
}
return {
terminated: false,
forced: killDelivered,
reason: killDelivered
? "process identity still exists after SIGKILL"
: "process exited before SIGKILL",
};
}
export function quoteArg(value) {
const text = String(value);
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) return text;
return `'${text.replace(/'/g, `'"'"'`)}'`;
}
export function renderCommand(argv, shell = false) {
if (shell) return argv[0] ?? "";
return argv.map(quoteArg).join(" ");
}
SHA-256: cfc88b3901122821bd1cc8713ba43a0dc42706a332d42768ddbadda2a233ec47