const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); // Add all bot usernames here. const USERNAMES = ["ZareMate", "Tomek", "Cytrus"]; 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 STARTUP_GAP_MIN_MS = 15 * 1000; const STARTUP_GAP_MAX_MS = 30 * 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 randomStartupGapMs() { return ( STARTUP_GAP_MIN_MS + Math.floor(Math.random() * (STARTUP_GAP_MAX_MS - STARTUP_GAP_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, startupTimer: null, sawKick: false, sawTimeout: false, sawAlreadyOnline: false, })); let startupOffset = 0; for (const state of botStates) { const delay = startupOffset; state.startupTimer = setTimeout(() => { state.startupTimer = null; if (isShuttingDown) return; startBot(state); }, delay); if (delay > 0) { const seconds = Math.round(delay / 1000); console.log(`[parent] startup delay for ${state.username}: ${seconds}s`); } startupOffset += randomStartupGapMs(); } function shutdownAll() { isShuttingDown = true; console.log("[parent] shutdown requested"); for (const state of botStates) { if (state.startupTimer) { clearTimeout(state.startupTimer); state.startupTimer = null; } 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);