Files
workinf_Blender_Wasm/tools/web/server-job-process.mjs
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

118 lines
4.4 KiB
JavaScript

import { spawn } from "node:child_process";
export const SERVER_JOB_PROCESS_SCHEMA = 1;
function invalid(message) {
throw new Error(`SERVER_JOB_PROCESS_INVALID: ${message}`);
}
function signalTree(pid, signal) {
if (!Number.isInteger(pid) || pid <= 0) return false;
if (process.platform === "win32") return false;
try {
process.kill(-pid, signal);
return true;
} catch (error) {
if (error?.code === "ESRCH") return false;
throw error;
}
}
function processTreeAlive(pid) {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(process.platform === "win32" ? pid : -pid, 0);
return true;
} catch (error) {
if (error?.code === "ESRCH") return false;
throw error;
}
}
async function waitForTreeExit(pid, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (processTreeAlive(pid) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
return !processTreeAlive(pid);
}
function waitForClose(child) {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve({ code: child.exitCode, signal: child.signalCode });
return new Promise((resolve) => child.once("close", (code, signal) => resolve({ code, signal })));
}
export function startServerJobProcess(command, args = [], options = {}) {
if (typeof command !== "string" || command.length === 0) invalid("command is required");
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) invalid("args must be strings");
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env,
detached: process.platform !== "win32",
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
windowsHide: true,
});
// Drain output even when the caller is testing cancellation before M13-04F receipt handling.
child.stdout?.resume();
child.stderr?.resume();
const receipt = {
schemaVersion: SERVER_JOB_PROCESS_SCHEMA,
pid: child.pid,
detached: process.platform !== "win32",
state: "RUNNING",
cancelRequested: false,
treeSignal: null,
result: null,
cleanupCount: 0,
orphanCount: 0,
};
const completion = waitForClose(child).then((result) => {
receipt.state = receipt.cancelRequested ? "CANCELLED" : "EXITED";
receipt.result = result;
return Object.freeze({ ...receipt });
});
return Object.freeze({ child, receipt, completion });
}
export async function cancelServerJobProcess(handle, cleanup, options = {}) {
if (!handle || !handle.child || !handle.receipt || handle.receipt.schemaVersion !== SERVER_JOB_PROCESS_SCHEMA) invalid("process handle is invalid");
if (typeof cleanup !== "function") invalid("cleanup callback is required");
const graceMs = Number.isSafeInteger(options.graceMs) && options.graceMs >= 0 ? options.graceMs : 500;
if (handle.receipt.state === "CANCELLED" || handle.receipt.state === "EXITED") {
if (handle.receipt.cleanupCount === 0) {
await cleanup();
handle.receipt.cleanupCount = 1;
}
return Object.freeze({ ...handle.receipt });
}
handle.receipt.cancelRequested = true;
if (process.platform === "win32") {
const treeSignal = spawn("taskkill", ["/PID", String(handle.receipt.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
handle.receipt.treeSignal = "TASKKILL_TREE";
await waitForClose(treeSignal);
} else {
handle.receipt.treeSignal = signalTree(handle.receipt.pid, "SIGTERM") ? "SIGTERM_GROUP" : "ALREADY_EXITED";
}
const settled = await Promise.race([
handle.completion,
new Promise((resolve) => setTimeout(() => resolve(null), graceMs)),
]);
if (!settled && process.platform !== "win32") {
signalTree(handle.receipt.pid, "SIGKILL");
handle.receipt.treeSignal = "SIGKILL_GROUP";
}
const result = settled ?? await handle.completion;
let treeExited = process.platform === "win32" || await waitForTreeExit(handle.receipt.pid, graceMs);
if (!treeExited && process.platform !== "win32") {
signalTree(handle.receipt.pid, "SIGKILL");
handle.receipt.treeSignal = "SIGKILL_GROUP";
treeExited = await waitForTreeExit(handle.receipt.pid, graceMs);
}
if (handle.receipt.cleanupCount === 0) {
await cleanup();
handle.receipt.cleanupCount = 1;
}
handle.receipt.orphanCount = treeExited ? 0 : 1;
return Object.freeze({ ...result, treeSignal: handle.receipt.treeSignal, cleanupCount: handle.receipt.cleanupCount, orphanCount: handle.receipt.orphanCount });
}