184 lines
5.1 KiB
JavaScript
184 lines
5.1 KiB
JavaScript
const { spawn } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
// Add all bot usernames here.
|
|
const USERNAMES = ["ZareMate", "Tomek"];
|
|
|
|
const AFK_SCRIPT = path.join(__dirname, "afk.js");
|
|
const OUTPUT_MODE = (
|
|
process.env.PARENT_OUTPUT_MODE || "prefixed"
|
|
).toLowerCase();
|
|
const SPLIT_LOGS_ENABLED = process.env.PARENT_SPLIT_LOGS === "1";
|
|
const RESTART_MIN_MS = 2 * 60 * 1000;
|
|
const RESTART_MAX_MS = 5 * 60 * 1000;
|
|
|
|
const ALREADY_ONLINE_TEXT = "You are already online, try restarting your game.";
|
|
const TIMEOUT_REGEX = /timeout|timed out|ETIMEDOUT|ECONNRESET|socket hang up/i;
|
|
|
|
let isShuttingDown = false;
|
|
|
|
function toSafeFilePart(value) {
|
|
return String(value).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
}
|
|
|
|
function writeSplitLog(logFile, line) {
|
|
if (!logFile) return;
|
|
const timestamp = new Date().toISOString();
|
|
fs.appendFileSync(logFile, `[${timestamp}] ${line}\n`);
|
|
}
|
|
|
|
function withUsernamePrefix(username, line) {
|
|
const prefix = `[${username}] `;
|
|
if (line.startsWith(prefix)) return line;
|
|
return `${prefix}${line}`;
|
|
}
|
|
|
|
function randomRestartDelayMs() {
|
|
return (
|
|
RESTART_MIN_MS +
|
|
Math.floor(Math.random() * (RESTART_MAX_MS - RESTART_MIN_MS + 1))
|
|
);
|
|
}
|
|
|
|
function shouldRestartFromState(state) {
|
|
if (state.sawKick) return true;
|
|
if (state.sawTimeout) return true;
|
|
if (state.sawAlreadyOnline) return true;
|
|
return false;
|
|
}
|
|
|
|
function updateRestartSignalsFromLine(state, line) {
|
|
if (!line) return;
|
|
|
|
if (line.includes("Kicked:")) {
|
|
state.sawKick = true;
|
|
}
|
|
if (TIMEOUT_REGEX.test(line)) {
|
|
state.sawTimeout = true;
|
|
}
|
|
if (line.includes(ALREADY_ONLINE_TEXT)) {
|
|
state.sawAlreadyOnline = true;
|
|
}
|
|
}
|
|
|
|
function startBot(state) {
|
|
const { username } = state;
|
|
const shouldPrefix = OUTPUT_MODE !== "raw";
|
|
const logFile = SPLIT_LOGS_ENABLED
|
|
? path.join(__dirname, `afk-parent-${toSafeFilePart(username)}.log`)
|
|
: null;
|
|
|
|
state.sawKick = false;
|
|
state.sawTimeout = false;
|
|
state.sawAlreadyOnline = false;
|
|
state.child = null;
|
|
|
|
const child = spawn(process.execPath, [AFK_SCRIPT, username], {
|
|
cwd: __dirname,
|
|
stdio: shouldPrefix ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
});
|
|
state.child = child;
|
|
|
|
console.log(`[parent] started ${username} (pid=${child.pid})`);
|
|
|
|
child.on("exit", (code, signal) => {
|
|
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
console.log(`[parent] ${username} exited (${reason})`);
|
|
|
|
if (isShuttingDown) return;
|
|
if (!shouldRestartFromState(state)) return;
|
|
|
|
const delay = randomRestartDelayMs();
|
|
const seconds = Math.round(delay / 1000);
|
|
console.log(`[parent] scheduling restart for ${username} in ${seconds}s`);
|
|
|
|
if (state.restartTimer) clearTimeout(state.restartTimer);
|
|
state.restartTimer = setTimeout(() => {
|
|
if (isShuttingDown) return;
|
|
startBot(state);
|
|
}, delay);
|
|
});
|
|
|
|
child.on("error", (err) => {
|
|
console.error(`[parent] failed to start ${username}: ${err.message}`);
|
|
});
|
|
|
|
if (shouldPrefix) {
|
|
let stdoutBuffer = "";
|
|
let stderrBuffer = "";
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
const text = stdoutBuffer + chunk.toString("utf8");
|
|
const lines = text.split(/\r?\n/);
|
|
stdoutBuffer = lines.pop() || "";
|
|
for (const line of lines) {
|
|
if (!line) continue;
|
|
updateRestartSignalsFromLine(state, line);
|
|
const rendered = withUsernamePrefix(username, line);
|
|
process.stdout.write(`${rendered}\n`);
|
|
writeSplitLog(logFile, `stdout: ${line}`);
|
|
}
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
const text = stderrBuffer + chunk.toString("utf8");
|
|
const lines = text.split(/\r?\n/);
|
|
stderrBuffer = lines.pop() || "";
|
|
for (const line of lines) {
|
|
if (!line) continue;
|
|
updateRestartSignalsFromLine(state, line);
|
|
const rendered = withUsernamePrefix(username, line);
|
|
process.stderr.write(`${rendered}\n`);
|
|
writeSplitLog(logFile, `stderr: ${line}`);
|
|
}
|
|
});
|
|
|
|
child.on("close", () => {
|
|
if (stdoutBuffer) {
|
|
updateRestartSignalsFromLine(state, stdoutBuffer);
|
|
const rendered = withUsernamePrefix(username, stdoutBuffer);
|
|
process.stdout.write(`${rendered}\n`);
|
|
writeSplitLog(logFile, `stdout: ${stdoutBuffer}`);
|
|
}
|
|
if (stderrBuffer) {
|
|
updateRestartSignalsFromLine(state, stderrBuffer);
|
|
const rendered = withUsernamePrefix(username, stderrBuffer);
|
|
process.stderr.write(`${rendered}\n`);
|
|
writeSplitLog(logFile, `stderr: ${stderrBuffer}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
return child;
|
|
}
|
|
|
|
const botStates = USERNAMES.map((username) => ({
|
|
username,
|
|
child: null,
|
|
restartTimer: null,
|
|
sawKick: false,
|
|
sawTimeout: false,
|
|
sawAlreadyOnline: false,
|
|
}));
|
|
|
|
for (const state of botStates) {
|
|
startBot(state);
|
|
}
|
|
|
|
function shutdownAll() {
|
|
isShuttingDown = true;
|
|
console.log("[parent] shutdown requested");
|
|
for (const state of botStates) {
|
|
if (state.restartTimer) {
|
|
clearTimeout(state.restartTimer);
|
|
state.restartTimer = null;
|
|
}
|
|
const child = state.child;
|
|
if (child && !child.killed) child.kill("SIGINT");
|
|
}
|
|
}
|
|
|
|
process.on("SIGINT", shutdownAll);
|
|
process.on("SIGTERM", shutdownAll);
|