diff options
Diffstat (limited to 'src/server/session_manager/session_manager.ts')
-rw-r--r-- | src/server/session_manager/session_manager.ts | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/src/server/session_manager/session_manager.ts b/src/server/session_manager/session_manager.ts new file mode 100644 index 000000000..6e9b03c79 --- /dev/null +++ b/src/server/session_manager/session_manager.ts @@ -0,0 +1,176 @@ +import * as request from "request-promise"; +import { log_execution, pathFromRoot } from "../ActionUtilities"; +import { red, yellow, cyan, green, Color } from "colors"; +import * as nodemailer from "nodemailer"; +import { MailOptions } from "nodemailer/lib/json-transport"; +import { writeFileSync, existsSync, mkdirSync } from "fs"; +import { resolve } from 'path'; +import { ChildProcess, exec, execSync } from "child_process"; +import InputManager from "./input_manager"; +import { identifier, logPath, crashPath, onWindows, pid, ports, heartbeat, recipient, LOCATION, latency } from "./config"; +const killport = require("kill-port"); + +process.on('SIGINT', endPrevious); + +const { registerCommand } = new InputManager({ identifier }); + +let manualRestartActive = false; +registerCommand("restart", [], async () => { + manualRestartActive = true; + identifiedLog(cyan("Initializing manual restart...")); + await endPrevious(); +}); + +registerCommand("exit", [], async () => { + identifiedLog(cyan("Initializing session end")); + await endPrevious(); + identifiedLog("Cleanup complete. Exiting session...\n"); + execSync(killAllCommand()); +}); + +if (!existsSync(logPath)) { + mkdirSync(logPath); +} +if (!existsSync(crashPath)) { + mkdirSync(crashPath); +} + +const crashLogPath = resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`); +function addLogEntry(message: string, color: Color) { + const formatted = color(`${message} ${timestamp()}.`); + identifiedLog(formatted); + // appendFileSync(crashLogPath, `${formatted}\n`); +} + +function identifiedLog(message?: any, ...optionalParams: any[]) { + console.log(identifier, message, ...optionalParams); +} + +if (!["win32", "darwin"].includes(process.platform)) { + identifiedLog(red("Invalid operating system: this script is supported only on Mac and Windows.")); + process.exit(1); +} + +let restarting = false; +let count = 0; + +function startServerCommand() { + if (onWindows) { + return '"C:\\Program Files\\Git\\git-bash.exe" -c "npm run start-release"'; + } + return `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && npm run start-release"\nend tell'`; +} + +function killAllCommand() { + if (onWindows) { + return "taskkill /f /im node.exe"; + } + return "killall -9 node"; +} + +identifiedLog("Initializing session..."); + +writeLocalPidLog("session_manager", pid); + +function writeLocalPidLog(filename: string, contents: any) { + const path = `./logs/current_${filename}_pid.log`; + identifiedLog(cyan(`${contents} written to ${path}`)); + writeFileSync(resolve(__dirname, path), `${contents}\n`); +} + +function timestamp() { + return `@ ${new Date().toISOString()}`; +} + +async function endPrevious() { + identifiedLog(yellow("Cleaning up previous connections...")); + current_backup?.kill("SIGKILL"); + await Promise.all(ports.map(port => { + const task = killport(port, 'tcp'); + return task.catch((error: any) => identifiedLog(red(error))); + })); + identifiedLog(yellow("Done. Any failures will be printed in red immediately above.")); +} + +let current_backup: ChildProcess | undefined = undefined; + +async function checkHeartbeat() { + let error: any; + try { + count && !restarting && process.stdout.write(`${identifier} 👂 `); + await request.get(heartbeat); + count && !restarting && console.log('⇠💚'); + if (restarting || manualRestartActive) { + addLogEntry(count++ ? "Backup server successfully restarted" : "Server successfully started", green); + restarting = false; + } + } catch (e) { + count && !restarting && console.log("⇠💔"); + error = e; + } finally { + if (error) { + if (!restarting || manualRestartActive) { + restarting = true; + if (count && !manualRestartActive) { + console.log(); + addLogEntry("Detected a server crash", red); + identifiedLog(red(error.message)); + await endPrevious(); + await log_execution({ + startMessage: identifier + " Sending crash notification email", + endMessage: ({ error, result }) => { + const success = error === null && result === true; + return identifier + ` ${(success ? `Notification successfully sent to` : `Failed to notify`)} ${recipient} ${timestamp()}`; + }, + action: async () => notify(error || "Hmm, no error to report..."), + color: cyan + }); + identifiedLog(green("Initiating server restart...")); + } + manualRestartActive = false; + current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || count ? "Previous server process exited." : "Spawned initial server.")); + writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`); + } + } + setTimeout(checkHeartbeat, 1000 * latency); + } +} + +async function startListening() { + identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`)); + if (!LOCATION) { + identifiedLog(red("No location specified for session manager. Please include as a command line environment variable or in a .env file.")); + process.exit(0); + } + await checkHeartbeat(); +} + +function emailText(error: any) { + return [ + `Hey ${recipient.split("@")[0]},`, + "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:", + `Location: ${LOCATION}\nError: ${error}`, + "The server should already be restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress." + ].join("\n\n"); +} + +async function notify(error: any) { + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + const mailOptions = { + to: recipient, + from: 'brownptcdash@gmail.com', + subject: 'Dash Server Crash', + text: emailText(error) + } as MailOptions; + return new Promise<boolean>(resolve => { + smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null)); + }); +} + +startListening();
\ No newline at end of file |