Files
edgeever/scripts/verify-packaged-desktop-startup.mjs

109 lines
4.4 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, statSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const requestedExecutablePath = process.argv[2]?.trim();
const executablePath = requestedExecutablePath ? resolve(requestedExecutablePath) : "";
if (!executablePath || !existsSync(executablePath) || !statSync(executablePath).isFile()) {
throw new Error(`Packaged desktop executable is missing: ${executablePath || "<not provided>"}`);
}
const userDataDirectory = mkdtempSync(join(tmpdir(), "edgeever-packaged-startup-"));
const diagnosticLogPath = join(userDataDirectory, "logs", "desktop.log");
const output = [];
let processError = null;
const child = spawn(executablePath, [`--user-data-dir=${userDataDirectory}`], {
env: {
...process.env,
EDGE_EVER_DISABLE_AUTO_UPDATE: "1",
},
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
const childClosed = new Promise((resolveClosed) => child.once("close", resolveClosed));
child.on("error", (error) => { processError = error; });
child.stdout.on("data", (chunk) => output.push(chunk.toString()));
child.stderr.on("data", (chunk) => output.push(chunk.toString()));
const timeoutMs = Number(process.env.EDGE_EVER_DESKTOP_STARTUP_TIMEOUT_MS) || 45_000;
const deadline = Date.now() + timeoutMs;
let startupReady = false;
let lastLog = "";
const requiredEvents = new Set(["renderer.origin-ready", "sidecar.ready", "renderer.bootstrap-ready"]);
const observedEvents = new Set();
const observedEntries = [];
const wait = (milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
const removeUserDataDirectory = async () => {
let lastError = null;
for (let attempt = 1; attempt <= 20; attempt += 1) {
try {
await rm(userDataDirectory, { recursive: true, force: true });
return;
} catch (error) {
lastError = error;
if (!["EBUSY", "ENOTEMPTY", "EPERM"].includes(error?.code)) break;
if (attempt < 20) await wait(250);
}
}
console.warn(`Temporary profile cleanup failed without changing the packaged startup result: ${lastError?.message || lastError}`);
};
try {
while (Date.now() < deadline) {
if (existsSync(diagnosticLogPath)) {
lastLog = readFileSync(diagnosticLogPath, "utf8");
for (const line of lastLog.split(/\r?\n/)) {
try {
const entry = JSON.parse(line);
const event = entry.event;
if (typeof event === "string") {
observedEvents.add(event);
observedEntries.push(entry);
}
} catch { /* Ignore a partial trailing log line. */ }
}
if ([...requiredEvents].every((event) => observedEvents.has(event))) {
startupReady = true;
break;
}
}
if (processError || child.exitCode !== null) break;
await wait(100);
}
if (!startupReady) {
throw new Error([
`Packaged desktop did not report renderer readiness within ${timeoutMs}ms (exit=${child.exitCode ?? "running"}).`,
`Missing events: ${[...requiredEvents].filter((event) => !observedEvents.has(event)).join(", ") || "none"}`,
processError && `Process error: ${processError.message}`,
lastLog && `Diagnostic log:\n${lastLog}`,
output.length > 0 && `Process output:\n${output.join("")}`,
].filter(Boolean).join("\n\n"));
}
const originEntry = observedEntries.find((entry) => entry.event === "renderer.origin-ready");
if (!originEntry || !["migrated", "already-migrated"].includes(originEntry.state)) {
throw new Error(`Packaged desktop did not complete renderer origin migration: ${JSON.stringify(originEntry)}`);
}
const rendererEntry = observedEntries.find((entry) =>
entry.event === "renderer.loaded" && String(entry.url || "").startsWith("edgeever-app://app/"));
if (!rendererEntry) {
throw new Error(`Packaged desktop did not load from the private app protocol.\n\nDiagnostic log:\n${lastLog}`);
}
console.log(JSON.stringify({ ok: true, executablePath, events: [...requiredEvents], rendererUrl: rendererEntry.url }));
} finally {
if (child.exitCode === null && child.pid) {
if (process.platform === "win32") {
spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
} else {
child.kill("SIGTERM");
}
await Promise.race([childClosed, wait(5_000)]);
}
await removeUserDataDirectory();
}