102 lines
3.1 KiB
JavaScript
102 lines
3.1 KiB
JavaScript
'use strict';
|
||
|
||
const DEFAULT_GRACE_MS = 15000;
|
||
|
||
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
|
||
const FATAL_OUTPUT = [
|
||
/---- Minecraft Crash Report ----/i,
|
||
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
|
||
/\b(?:This crash report has been saved to|Saving crash report to)\b/i,
|
||
/\bA fatal error has been detected by the Java Runtime Environment\b/i,
|
||
/Exception in thread "(?:Render thread|Client thread|main)"/i,
|
||
/\[(?:Render thread|Client thread|main)\/FATAL\]/i,
|
||
];
|
||
|
||
function createProcessWatchdog(options) {
|
||
const graceMs = options.graceMs || DEFAULT_GRACE_MS;
|
||
const schedule = options.setTimer || setTimeout;
|
||
const cancel = options.clearTimer || clearTimeout;
|
||
const buffers = { stdout: '', stderr: '' };
|
||
let timer = null;
|
||
let armed = false;
|
||
let crashed = false;
|
||
let ended = false;
|
||
let forced = false;
|
||
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
|
||
// wurde – im Gegensatz zu `armed` wird das NICHT durch markCrash()
|
||
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
|
||
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
|
||
let sawCleanShutdown = false;
|
||
|
||
function clear() {
|
||
if (timer !== null) cancel(timer);
|
||
timer = null;
|
||
armed = false;
|
||
}
|
||
|
||
function markCrash() {
|
||
crashed = true;
|
||
clear();
|
||
}
|
||
|
||
function scheduleKill() {
|
||
if (timer !== null) cancel(timer);
|
||
timer = schedule(() => {
|
||
timer = null;
|
||
if (ended || crashed || !armed || !options.isRunning()) return;
|
||
try {
|
||
forced = options.kill() === true;
|
||
} catch {
|
||
forced = false;
|
||
}
|
||
if (forced && options.onForce) options.onForce(graceMs);
|
||
else if (!forced && options.isRunning() && options.onError) options.onError();
|
||
}, graceMs);
|
||
if (timer && typeof timer.unref === 'function') timer.unref();
|
||
}
|
||
|
||
function inspectLine(line) {
|
||
const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
|
||
if (isCleanShutdown) sawCleanShutdown = true;
|
||
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
|
||
markCrash();
|
||
return;
|
||
}
|
||
if (ended || crashed || armed || !isCleanShutdown) return;
|
||
|
||
armed = true;
|
||
scheduleKill();
|
||
}
|
||
|
||
function feed(chunk, stream) {
|
||
if (ended) return;
|
||
const key = stream === 'stderr' ? 'stderr' : 'stdout';
|
||
const text = buffers[key] + String(chunk || '');
|
||
const lines = text.split(/\r?\n/);
|
||
buffers[key] = lines.pop() || '';
|
||
for (const line of lines) inspectLine(line);
|
||
if (armed && !crashed && !ended && lines.length) scheduleKill();
|
||
}
|
||
|
||
function end() {
|
||
ended = true;
|
||
clear();
|
||
}
|
||
|
||
return {
|
||
feed,
|
||
end,
|
||
get armed() { return armed; },
|
||
get crashed() { return crashed; },
|
||
get forced() { return forced; },
|
||
get sawCleanShutdown() { return sawCleanShutdown; },
|
||
};
|
||
}
|
||
|
||
module.exports = {
|
||
DEFAULT_GRACE_MS,
|
||
CLEAN_SHUTDOWN,
|
||
FATAL_OUTPUT,
|
||
createProcessWatchdog,
|
||
};
|