mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
423 lines
13 KiB
JavaScript
423 lines
13 KiB
JavaScript
const path = require("node:path");
|
|
const { spawn: defaultSpawn } = require("child_process");
|
|
|
|
const { verifyBackendIntegrity } = require("./backendIntegrity");
|
|
const {
|
|
clearCrashReport,
|
|
getDiagnosticPaths,
|
|
getLogsDir,
|
|
loadCrashReport,
|
|
persistCrashReport,
|
|
} = require("./backendCrashReport");
|
|
const { killOrphanBackendProcesses } = require("./backendProcessOrphans");
|
|
|
|
const LOG_LINE_CAP = 200;
|
|
|
|
function createInitialRuntimeState() {
|
|
return {
|
|
started: false,
|
|
running: false,
|
|
pid: null,
|
|
lastExitCode: null,
|
|
lastError: "",
|
|
lastEventAt: null,
|
|
};
|
|
}
|
|
|
|
function createBackendProcessManager(deps) {
|
|
const {
|
|
log,
|
|
getDefaultStorageDir,
|
|
getDefaultReticulumConfigDir,
|
|
getMainWindowPageKind,
|
|
notifyRenderer,
|
|
showCrashPage,
|
|
spawn: spawnFn = defaultSpawn,
|
|
} = deps;
|
|
|
|
let childProcess = null;
|
|
let runtimeState = createInitialRuntimeState();
|
|
let logBuffers = { stdout: [], stderr: [] };
|
|
let lastCrash = null;
|
|
let resolvedExePath = null;
|
|
let userProvidedArguments = [];
|
|
|
|
const storageDir = () => getDefaultStorageDir();
|
|
const reticulumConfigDir = () => getDefaultReticulumConfigDir();
|
|
|
|
function hydratePersistedCrash() {
|
|
const persisted = loadCrashReport(storageDir());
|
|
if (persisted) {
|
|
lastCrash = persisted;
|
|
}
|
|
}
|
|
hydratePersistedCrash();
|
|
|
|
function isRunning() {
|
|
return !!childProcess && childProcess.exitCode === null && childProcess.signalCode === null;
|
|
}
|
|
|
|
function getRuntimeState() {
|
|
return {
|
|
...runtimeState,
|
|
running: isRunning() && runtimeState.started,
|
|
};
|
|
}
|
|
|
|
function pushLogLine(buffer, line) {
|
|
buffer.push(line);
|
|
if (buffer.length > LOG_LINE_CAP) {
|
|
buffer.shift();
|
|
}
|
|
}
|
|
|
|
function getJoinedLogs() {
|
|
return {
|
|
stdout: logBuffers.stdout.join(""),
|
|
stderr: logBuffers.stderr.join(""),
|
|
};
|
|
}
|
|
|
|
function getLastCrash() {
|
|
return lastCrash;
|
|
}
|
|
|
|
function getStartupDiagnostics() {
|
|
const paths = getDiagnosticPaths(storageDir(), reticulumConfigDir());
|
|
return {
|
|
runtime: getRuntimeState(),
|
|
crash: lastCrash,
|
|
paths,
|
|
};
|
|
}
|
|
|
|
function setUserProvidedArguments(args) {
|
|
userProvidedArguments = Array.isArray(args) ? args : [];
|
|
}
|
|
|
|
function resolveExecutablePath(findExePath) {
|
|
resolvedExePath = findExePath();
|
|
return resolvedExePath;
|
|
}
|
|
|
|
function recordCrash(code) {
|
|
const logs = getJoinedLogs();
|
|
lastCrash = {
|
|
code,
|
|
stdout: logs.stdout,
|
|
stderr: logs.stderr,
|
|
at: Date.now(),
|
|
pid: runtimeState.pid,
|
|
platform: process.platform,
|
|
};
|
|
try {
|
|
persistCrashReport(storageDir(), lastCrash);
|
|
} catch (error) {
|
|
log(`Failed to persist backend crash report: ${error && error.message ? error.message : error}`);
|
|
}
|
|
}
|
|
|
|
function notifyStartupFailure(code) {
|
|
const paths = getDiagnosticPaths(storageDir(), reticulumConfigDir());
|
|
notifyRenderer("backend-startup-failed", {
|
|
code,
|
|
at: lastCrash?.at ?? Date.now(),
|
|
stdout: lastCrash?.stdout || "",
|
|
stderr: lastCrash?.stderr || "",
|
|
paths,
|
|
});
|
|
notifyRenderer("backend-process-exited", { code, at: lastCrash?.at ?? Date.now() });
|
|
}
|
|
|
|
function attachChildHandlers(proc) {
|
|
logBuffers = { stdout: [], stderr: [] };
|
|
|
|
proc.stdout.setEncoding("utf8");
|
|
proc.stdout.on("data", (data) => {
|
|
const text = data.toString();
|
|
log(text);
|
|
pushLogLine(logBuffers.stdout, text);
|
|
});
|
|
|
|
proc.stderr.setEncoding("utf8");
|
|
proc.stderr.on("data", (data) => {
|
|
const text = data.toString();
|
|
log(text);
|
|
pushLogLine(logBuffers.stderr, text);
|
|
});
|
|
|
|
proc.on("error", (error) => {
|
|
log(error);
|
|
runtimeState.lastError = error && error.message ? error.message : String(error);
|
|
runtimeState.lastEventAt = Date.now();
|
|
});
|
|
|
|
proc.on("exit", async (code) => {
|
|
runtimeState.running = false;
|
|
runtimeState.lastExitCode = code;
|
|
runtimeState.lastEventAt = Date.now();
|
|
childProcess = null;
|
|
|
|
if (code == null || deps.isQuiting()) {
|
|
return;
|
|
}
|
|
|
|
recordCrash(code);
|
|
|
|
const page = getMainWindowPageKind();
|
|
if (page === "loading") {
|
|
notifyStartupFailure(code);
|
|
return;
|
|
}
|
|
|
|
notifyRenderer("backend-process-exited", { code, at: lastCrash.at });
|
|
|
|
if (page === "app") {
|
|
return;
|
|
}
|
|
|
|
if (page === "crash") {
|
|
return;
|
|
}
|
|
|
|
await showCrashPage(lastCrash);
|
|
});
|
|
}
|
|
|
|
function buildSpawnEnv() {
|
|
const logsDir = getLogsDir(storageDir());
|
|
return {
|
|
...process.env,
|
|
MESHCHAT_LOG_DIR: logsDir,
|
|
MESHCHAT_STORAGE_DIR: storageDir(),
|
|
MESHCHAT_RETICULUM_CONFIG_DIR: reticulumConfigDir(),
|
|
};
|
|
}
|
|
|
|
function shouldUseAppContainerLauncher() {
|
|
if (process.platform !== "win32") {
|
|
return false;
|
|
}
|
|
const raw = process.env.MESHCHAT_APPCONTAINER;
|
|
if (raw !== undefined && raw !== null && String(raw).trim() !== "") {
|
|
const val = String(raw).trim().toLowerCase();
|
|
if (["false", "0", "no", "off"].includes(val)) {
|
|
return false;
|
|
}
|
|
if (["true", "1", "yes", "on"].includes(val)) {
|
|
return true;
|
|
}
|
|
}
|
|
// Opt-in only: set MESHCHAT_APPCONTAINER=1 to use the AppContainer launcher.
|
|
return false;
|
|
}
|
|
|
|
function buildSpawnArgs(extraArgs = []) {
|
|
const backendArgs = buildBackendArgs(extraArgs);
|
|
if (!shouldUseAppContainerLauncher()) {
|
|
return backendArgs;
|
|
}
|
|
return ["--meshchatx-run-module", "meshchatx.src.backend.appcontainer_launcher", ...backendArgs];
|
|
}
|
|
|
|
async function spawnBackend(exePath, integrityStatusRef) {
|
|
if (!exePath) {
|
|
throw new Error("Backend executable path is not set.");
|
|
}
|
|
if (isRunning()) {
|
|
return { ok: true, alreadyRunning: true };
|
|
}
|
|
|
|
const removed = killOrphanBackendProcesses(null);
|
|
if (removed > 0) {
|
|
log(`Removed ${removed} orphan backend process(es) before startup.`);
|
|
}
|
|
|
|
resolvedExePath = exePath;
|
|
const exeDir = path.dirname(exePath);
|
|
integrityStatusRef.backend = verifyBackendIntegrity(exeDir);
|
|
if (
|
|
integrityStatusRef.backend.ok &&
|
|
integrityStatusRef.backend.issues.length === 1 &&
|
|
integrityStatusRef.backend.issues[0] === "Manifest missing"
|
|
) {
|
|
log("Backend integrity manifest missing, skipping check.");
|
|
}
|
|
if (!integrityStatusRef.backend.ok) {
|
|
log(
|
|
`INTEGRITY WARNING: Backend tampering detected! Issues: ${integrityStatusRef.backend.issues.join(", ")}`
|
|
);
|
|
}
|
|
|
|
if (shouldUseAppContainerLauncher()) {
|
|
log("Starting Windows backend via AppContainer launcher.");
|
|
}
|
|
|
|
const proc = spawnFn(exePath, buildSpawnArgs(), {
|
|
env: buildSpawnEnv(),
|
|
windowsHide: true,
|
|
});
|
|
if (!proc || !proc.pid) {
|
|
throw new Error("Failed to start backend process (no PID).");
|
|
}
|
|
|
|
childProcess = proc;
|
|
runtimeState = {
|
|
started: true,
|
|
running: true,
|
|
pid: proc.pid,
|
|
lastExitCode: null,
|
|
lastError: "",
|
|
lastEventAt: Date.now(),
|
|
};
|
|
attachChildHandlers(proc);
|
|
return { ok: true, pid: proc.pid };
|
|
}
|
|
|
|
function getChildProcess() {
|
|
return childProcess;
|
|
}
|
|
|
|
function killChild(signal) {
|
|
if (!childProcess) {
|
|
return;
|
|
}
|
|
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
|
|
return;
|
|
}
|
|
if (process.platform === "win32") {
|
|
try {
|
|
const { execFileSync } = require("node:child_process");
|
|
execFileSync("taskkill", ["/F", "/T", "/PID", String(childProcess.pid)], {
|
|
stdio: "ignore",
|
|
windowsHide: true,
|
|
});
|
|
} catch (error) {
|
|
log(error);
|
|
}
|
|
return;
|
|
}
|
|
childProcess.kill(signal);
|
|
}
|
|
|
|
async function restartBackend(integrityStatusRef) {
|
|
if (!resolvedExePath) {
|
|
return { ok: false, error: "Backend executable is not configured." };
|
|
}
|
|
if (isRunning()) {
|
|
return { ok: false, error: "Backend is already running." };
|
|
}
|
|
try {
|
|
const result = await spawnBackend(resolvedExePath, integrityStatusRef);
|
|
return { ok: true, pid: result.pid };
|
|
} catch (error) {
|
|
return { ok: false, error: error && error.message ? error.message : String(error) };
|
|
}
|
|
}
|
|
|
|
function buildBackendArgs(extraArgs = []) {
|
|
const requiredArguments = ["--headless", "--port", "9337"];
|
|
if (!userProvidedArguments.includes("--reticulum-config-dir")) {
|
|
requiredArguments.push("--reticulum-config-dir", reticulumConfigDir());
|
|
}
|
|
if (!userProvidedArguments.includes("--storage-dir")) {
|
|
requiredArguments.push("--storage-dir", storageDir());
|
|
}
|
|
return [...requiredArguments, ...userProvidedArguments, ...extraArgs];
|
|
}
|
|
|
|
async function runMaintenanceTask(extraArgs) {
|
|
if (!resolvedExePath) {
|
|
return { ok: false, error: "Backend executable is not configured." };
|
|
}
|
|
if (isRunning()) {
|
|
killChild("SIGTERM");
|
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
}
|
|
|
|
return await new Promise((resolve) => {
|
|
const stdoutChunks = [];
|
|
const stderrChunks = [];
|
|
const proc = spawnFn(resolvedExePath, buildSpawnArgs(extraArgs), {
|
|
env: buildSpawnEnv(),
|
|
windowsHide: true,
|
|
});
|
|
if (!proc || !proc.pid) {
|
|
resolve({ ok: false, error: "Failed to start backend maintenance task." });
|
|
return;
|
|
}
|
|
|
|
proc.stdout?.on("data", (chunk) => {
|
|
stdoutChunks.push(String(chunk));
|
|
});
|
|
proc.stderr?.on("data", (chunk) => {
|
|
stderrChunks.push(String(chunk));
|
|
});
|
|
proc.on("error", (error) => {
|
|
resolve({
|
|
ok: false,
|
|
error: error && error.message ? error.message : String(error),
|
|
stdout: stdoutChunks.join(""),
|
|
stderr: stderrChunks.join(""),
|
|
});
|
|
});
|
|
proc.on("exit", (code) => {
|
|
const stdout = stdoutChunks.join("");
|
|
const stderr = stderrChunks.join("");
|
|
if (code === 0) {
|
|
resolve({ ok: true, exitCode: code, stdout, stderr });
|
|
return;
|
|
}
|
|
resolve({
|
|
ok: false,
|
|
exitCode: code,
|
|
error: `Maintenance task exited with code ${code}`,
|
|
stdout,
|
|
stderr,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function getResolvedExecutablePath() {
|
|
return resolvedExePath;
|
|
}
|
|
|
|
async function openCrashReport(showCrashPageFn) {
|
|
if (!lastCrash) {
|
|
return { ok: false, error: "No backend crash report is available." };
|
|
}
|
|
await showCrashPageFn(lastCrash);
|
|
return { ok: true };
|
|
}
|
|
|
|
function markBackendHealthy() {
|
|
runtimeState.lastExitCode = null;
|
|
runtimeState.lastError = "";
|
|
clearCrashReport(storageDir());
|
|
lastCrash = null;
|
|
}
|
|
|
|
return {
|
|
createInitialRuntimeState,
|
|
setUserProvidedArguments,
|
|
resolveExecutablePath,
|
|
spawnBackend,
|
|
restartBackend,
|
|
openCrashReport,
|
|
getRuntimeState,
|
|
getLastCrash,
|
|
getStartupDiagnostics,
|
|
markBackendHealthy,
|
|
getChildProcess,
|
|
isRunning,
|
|
killChild,
|
|
getJoinedLogs,
|
|
runMaintenanceTask,
|
|
getResolvedExecutablePath,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createBackendProcessManager,
|
|
};
|