DonutBot/afk_parent.js

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);