156 lines
5.3 KiB
JavaScript
156 lines
5.3 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { readFile, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { runPendingNukeCleanup } from "./nuke.mjs";
|
|
|
|
const WORKER_RETRY_INTERVAL_MS = 500;
|
|
const RELAUNCH_HANDLE_GRACE_MS = 500;
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function pathApi(platform) {
|
|
return platform === "win32" ? path.win32 : path.posix;
|
|
}
|
|
|
|
function isAbsoluteForPlatform(value, platform) {
|
|
return pathApi(platform).isAbsolute(String(value ?? ""));
|
|
}
|
|
|
|
function parentIsAlive(pid) {
|
|
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return error?.code === "EPERM";
|
|
}
|
|
}
|
|
|
|
function errorMessage(error) {
|
|
if (error instanceof Error) return error.message;
|
|
return String(error);
|
|
}
|
|
|
|
export function validateNukeWorkerPayload(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error("Invalid nuke worker payload.");
|
|
}
|
|
const payload = value;
|
|
const nukeInput = payload.nukeInput;
|
|
if (!nukeInput || typeof nukeInput !== "object" || Array.isArray(nukeInput)) {
|
|
throw new Error("Invalid nuke worker nukeInput.");
|
|
}
|
|
const platform = nukeInput.platform === "win32" || nukeInput.platform === "darwin" || nukeInput.platform === "linux"
|
|
? nukeInput.platform
|
|
: "linux";
|
|
const homedir = typeof nukeInput.homedir === "string" ? nukeInput.homedir : "";
|
|
const userDataPath = typeof nukeInput.userDataPath === "string" ? nukeInput.userDataPath : "";
|
|
const appExecutablePath = typeof payload.appExecutablePath === "string" ? payload.appExecutablePath : "";
|
|
if (!isAbsoluteForPlatform(homedir, platform) || !isAbsoluteForPlatform(userDataPath, platform)) {
|
|
throw new Error("Nuke worker paths must be absolute.");
|
|
}
|
|
if (!path.isAbsolute(appExecutablePath)) {
|
|
throw new Error("Nuke worker app executable path must be absolute.");
|
|
}
|
|
const appArgv = Array.isArray(payload.appArgv) ? payload.appArgv.filter((arg) => typeof arg === "string") : [];
|
|
const parentPid = Number(payload.parentPid) || 0;
|
|
const now = Date.now();
|
|
const parentWaitDeadlineAt = Number.isFinite(payload.parentWaitDeadlineAt) ? Number(payload.parentWaitDeadlineAt) : now;
|
|
const deadlineAt = Number.isFinite(payload.deadlineAt) ? Number(payload.deadlineAt) : now;
|
|
return {
|
|
version: 1,
|
|
parentPid,
|
|
nukeInput: {
|
|
env: nukeInput.env && typeof nukeInput.env === "object" && !Array.isArray(nukeInput.env) ? nukeInput.env : {},
|
|
homedir,
|
|
platform,
|
|
preserveBootstrap: nukeInput.preserveBootstrap !== false,
|
|
userDataPath,
|
|
workspacePaths: Array.isArray(nukeInput.workspacePaths)
|
|
? nukeInput.workspacePaths.filter((entry) => typeof entry === "string" && entry.trim())
|
|
: [],
|
|
},
|
|
appExecutablePath,
|
|
appArgv,
|
|
pendingPath: typeof payload.pendingPath === "string" ? payload.pendingPath : "",
|
|
parentWaitDeadlineAt,
|
|
deadlineAt,
|
|
};
|
|
}
|
|
|
|
async function waitForParentExit(parentPid, deadlineAt, deps) {
|
|
const sleepFn = deps.sleep ?? sleep;
|
|
while (Date.now() < deadlineAt && parentIsAlive(parentPid)) {
|
|
await sleepFn(WORKER_RETRY_INTERVAL_MS);
|
|
}
|
|
}
|
|
|
|
async function cleanupUntilDeadline(payload, deps) {
|
|
const sleepFn = deps.sleep ?? sleep;
|
|
const runPendingCleanup = deps.runPendingCleanup ?? runPendingNukeCleanup;
|
|
let result = await runPendingCleanup(payload.nukeInput);
|
|
while (result.pendingRetry.length > 0 && Date.now() < payload.deadlineAt) {
|
|
await sleepFn(WORKER_RETRY_INTERVAL_MS);
|
|
result = await runPendingCleanup(payload.nukeInput);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function boundedMs(value, fallback) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
}
|
|
|
|
async function waitForHandleReleaseGrace(deps) {
|
|
const sleepFn = deps.sleep ?? sleep;
|
|
const graceMs = boundedMs(deps.relaunchHandleGraceMs, RELAUNCH_HANDLE_GRACE_MS);
|
|
if (graceMs > 0) await sleepFn(graceMs);
|
|
}
|
|
|
|
function launchApp(payload, deps) {
|
|
const spawnApp = deps.spawnApp ?? spawn;
|
|
const env = { ...(deps.env ?? process.env) };
|
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
delete env["OPENWORK_ELECTRON_REMOTE_DEBUG_PORT"];
|
|
const child = spawnApp(payload.appExecutablePath, payload.appArgv, {
|
|
detached: true,
|
|
env,
|
|
stdio: "ignore",
|
|
windowsHide: true,
|
|
});
|
|
child.unref?.();
|
|
return child.pid ?? null;
|
|
}
|
|
|
|
export async function runNukeCleanupWorker(payloadPath, deps = {}) {
|
|
const raw = JSON.parse(await readFile(payloadPath, "utf8"));
|
|
const payload = validateNukeWorkerPayload(raw);
|
|
await waitForParentExit(payload.parentPid, payload.parentWaitDeadlineAt, deps);
|
|
let cleanup = null;
|
|
try {
|
|
cleanup = await cleanupUntilDeadline(payload, deps);
|
|
} catch (error) {
|
|
cleanup = {
|
|
ran: false,
|
|
invalid: false,
|
|
deleted: [],
|
|
pendingRetry: [],
|
|
errors: [{ path: "nuke-cleanup-worker", message: errorMessage(error) }],
|
|
};
|
|
}
|
|
await waitForHandleReleaseGrace(deps);
|
|
await rm(payloadPath, { force: true }).catch(() => undefined);
|
|
const launchPid = launchApp(payload, deps);
|
|
return { cleanup, launchPid };
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
runNukeCleanupWorker(String(process.argv[2] ?? "")).catch(() => {
|
|
process.exitCode = 1;
|
|
});
|
|
}
|