From b31d54b285236dc92f7d287af6a441878f429a34 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 3 Jan 2020 23:28:34 -0800 Subject: session restructuring and schema enforced json configuration --- src/server/ActionUtilities.ts | 26 +++ src/server/Session/session.ts | 168 +++++++++++++++++ src/server/Session/session_config_schema.ts | 25 +++ src/server/index.ts | 2 +- src/server/repl.ts | 103 +++++++++++ src/server/session.ts | 142 -------------- src/server/session_manager/config.ts | 33 ---- src/server/session_manager/email.ts | 26 --- src/server/session_manager/input_manager.ts | 103 ----------- .../session_manager/logs/current_daemon_pid.log | 1 - .../session_manager/logs/current_server_pid.log | 1 - .../logs/current_session_manager_pid.log | 1 - src/server/session_manager/session_manager.ts | 206 --------------------- .../session_manager/session_manager_cluster.ts | 36 ---- 14 files changed, 323 insertions(+), 550 deletions(-) create mode 100644 src/server/Session/session.ts create mode 100644 src/server/Session/session_config_schema.ts create mode 100644 src/server/repl.ts delete mode 100644 src/server/session.ts delete mode 100644 src/server/session_manager/config.ts delete mode 100644 src/server/session_manager/email.ts delete mode 100644 src/server/session_manager/input_manager.ts delete mode 100644 src/server/session_manager/logs/current_daemon_pid.log delete mode 100644 src/server/session_manager/logs/current_server_pid.log delete mode 100644 src/server/session_manager/logs/current_session_manager_pid.log delete mode 100644 src/server/session_manager/session_manager.ts delete mode 100644 src/server/session_manager/session_manager_cluster.ts (limited to 'src') diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index 053576a92..950fba093 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -4,6 +4,8 @@ import { exec } from 'child_process'; import * as path from 'path'; import * as rimraf from "rimraf"; import { yellow, Color } from 'colors'; +import * as nodemailer from "nodemailer"; +import { MailOptions } from "nodemailer/lib/json-transport"; const projectRoot = path.resolve(__dirname, "../../"); export function pathFromRoot(relative?: string) { @@ -105,3 +107,27 @@ export async function Prune(rootDirectory: string): Promise { } export const Destroy = (mediaPath: string) => new Promise(resolve => unlink(mediaPath, error => resolve(error === null))); + +export namespace Email { + + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + + export async function dispatch(recipient: string, subject: string, content: string): Promise { + const mailOptions = { + to: recipient, + from: 'brownptcdash@gmail.com', + subject, + text: `Hello ${recipient.split("@")[0]},\n\n${content}` + } as MailOptions; + return new Promise(resolve => { + smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null)); + }); + } + +} \ No newline at end of file diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts new file mode 100644 index 000000000..6d8ce53c0 --- /dev/null +++ b/src/server/Session/session.ts @@ -0,0 +1,168 @@ +import { red, cyan, green, yellow, magenta } from "colors"; +import { isMaster, on, fork, setupMaster, Worker } from "cluster"; +import { execSync } from "child_process"; +import { get } from "request-promise"; +import { WebSocket } from "../Websocket/Websocket"; +import { Utils } from "../../Utils"; +import { MessageStore } from "../Message"; +import { Email } from "../ActionUtilities"; +import Repl from "../repl"; +import { readFileSync } from "fs"; +import { validate, ValidationError } from "jsonschema"; +import { configurationSchema } from "./session_config_schema"; + +const onWindows = process.platform === "win32"; + +export namespace Session { + + const { masterIdentifier, workerIdentifier, recipients, signature, heartbeat, silentChildren } = loadConfiguration(); + export let key: string; + let activeWorker: Worker; + let listening = false; + + function loadConfiguration() { + try { + const raw = readFileSync('./session.config.json', 'utf8'); + const configuration = JSON.parse(raw); + const options = { + throwError: true, + allowUnknownAttributes: false + }; + validate(configuration, configurationSchema, options); + configuration.masterIdentifier = `${yellow(configuration.masterIdentifier)}:`; + configuration.workerIdentifier = `${magenta(configuration.workerIdentifier)}:`; + return configuration; + } catch (error) { + console.log(red("\nSession configuration failed.")); + if (error instanceof ValidationError) { + console.log("The given session.config.json configuration file is invalid."); + console.log(`${error.instance}: ${error.stack}`); + } else if (error.code === "ENOENT" && error.path === "./session.config.json") { + console.log("Please include a session.config.json configuration file in your project root."); + } else { + console.log(error.stack); + } + console.log(); + process.exit(0); + } + } + + function log(message?: any, ...optionalParams: any[]) { + const identifier = isMaster ? masterIdentifier : workerIdentifier; + console.log(identifier, message, ...optionalParams); + } + + export async function distributeKey() { + key = Utils.GenerateGuid(); + const timestamp = new Date().toUTCString(); + const content = `The key for this session (started @ ${timestamp}) is ${key}.\n\n${signature}`; + return Promise.all(recipients.map((recipient: string) => Email.dispatch(recipient, "Server Termination Key", content))); + } + + function tryKillActiveWorker() { + if (activeWorker && !activeWorker.isDead()) { + activeWorker.process.kill(); + return true; + } + return false; + } + + function logLifecycleEvent(lifecycle: string) { + process.send?.({ lifecycle }); + } + + function messageHandler({ lifecycle, action }: any) { + if (action) { + console.log(`${workerIdentifier} action requested (${action})`); + switch (action) { + case "kill": + log(red("An authorized user has ended the server from the /kill route")); + tryKillActiveWorker(); + process.exit(0); + } + } else if (lifecycle) { + console.log(`${workerIdentifier} lifecycle phase (${lifecycle})`); + } + } + + async function activeExit(error: Error) { + if (!listening) { + return; + } + listening = false; + await Promise.all(recipients.map((recipient: string) => Email.dispatch(recipient, "Dash Web Server Crash", crashReport(error)))); + const { _socket } = WebSocket; + if (_socket) { + Utils.Emit(_socket, MessageStore.ConnectionTerminated, "Manual"); + } + logLifecycleEvent(red(`Crash event detected @ ${new Date().toUTCString()}`)); + logLifecycleEvent(red(error.message)); + process.exit(1); + } + + function crashReport({ name, message, stack }: Error) { + return [ + "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:", + `name:\n${name}`, + `message:\n${message}`, + `stack:\n${stack}`, + "The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.", + signature + ].join("\n\n"); + } + + export async function initialize(work: Function) { + if (isMaster) { + process.on("uncaughtException", error => { + if (error.message !== "Channel closed") { + log(red(error.message)); + if (error.stack) { + log(`\n${red(error.stack)}`); + } + } + }); + setupMaster({ silent: silentChildren }); + const spawn = () => { + tryKillActiveWorker(); + activeWorker = fork(); + activeWorker.on("message", messageHandler); + }; + spawn(); + on("exit", ({ process: { pid } }, code, signal) => { + const prompt = `Server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; + log(cyan(prompt)); + spawn(); + }); + const { registerCommand } = new Repl({ identifier: masterIdentifier }); + registerCommand("exit", [], () => execSync(onWindows ? "taskkill /f /im node.exe" : "killall -9 node")); + registerCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] })); + registerCommand("restart", [], () => { + listening = false; + tryKillActiveWorker(); + }); + } else { + logLifecycleEvent(green("initializing...")); + process.on('uncaughtException', activeExit); + const checkHeartbeat = async () => { + await new Promise(resolve => { + setTimeout(async () => { + try { + await get(heartbeat); + if (!listening) { + logLifecycleEvent(green("listening...")); + } + listening = true; + resolve(); + } catch (error) { + await activeExit(error); + } + }, 1000 * 15); + }); + checkHeartbeat(); + }; + work(); + checkHeartbeat(); + } + } + +} \ No newline at end of file diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts new file mode 100644 index 000000000..25d95c243 --- /dev/null +++ b/src/server/Session/session_config_schema.ts @@ -0,0 +1,25 @@ +import { Schema } from "jsonschema"; + +export const configurationSchema: Schema = { + id: "/Configuration", + type: "object", + properties: { + recipients: { + type: "array", + items: { + type: "string", + pattern: /[^\@]+\@[^\@]+/g + }, + minLength: 1 + }, + heartbeat: { + type: "string", + pattern: /http\:\/\/localhost:\d+\/[a-zA-Z]+/g + }, + signature: { type: "string" }, + masterIdentifier: { type: "string", minLength: 1 }, + workerIdentifier: { type: "string", minLength: 1 }, + silentChildren: { type: "boolean" } + }, + required: ["heartbeat", "recipients", "signature", "masterIdentifier", "workerIdentifier", "silentChildren"] +}; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 8706c2d84..88bab7dea 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -23,7 +23,7 @@ import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; import { Logger } from "./ProcessFactory"; import { yellow } from "colors"; -import { Session } from "./session"; +import { Session } from "./Session/session"; import { Utils } from "../Utils"; export const publicDirectory = path.resolve(__dirname, "public"); diff --git a/src/server/repl.ts b/src/server/repl.ts new file mode 100644 index 000000000..ec525582b --- /dev/null +++ b/src/server/repl.ts @@ -0,0 +1,103 @@ +import { createInterface, Interface } from "readline"; +import { red } from "colors"; + +export interface Configuration { + identifier: string; + onInvalid?: (culprit?: string) => string | string; + isCaseSensitive?: boolean; +} + +type Action = (parsedArgs: IterableIterator) => any | Promise; +export interface Registration { + argPatterns: RegExp[]; + action: Action; +} + +export default class Repl { + private identifier: string; + private onInvalid: ((culprit?: string) => string) | string; + private isCaseSensitive: boolean; + private commandMap = new Map(); + public interface: Interface; + private busy = false; + private keys: string | undefined; + + constructor({ identifier: prompt, onInvalid, isCaseSensitive }: Configuration) { + this.identifier = prompt; + this.onInvalid = onInvalid || this.usage; + this.isCaseSensitive = isCaseSensitive ?? true; + this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); + } + + private usage = () => { + const resolved = this.keys; + if (resolved) { + return resolved; + } + const members: string[] = []; + const keys = this.commandMap.keys(); + let next: IteratorResult; + while (!(next = keys.next()).done) { + members.push(next.value); + } + return `${this.identifier} commands: { ${members.sort().join(", ")} }`; + } + + public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: Action) => { + const existing = this.commandMap.get(basename); + const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); + const registration = { argPatterns: converted, action }; + if (existing) { + existing.push(registration); + } else { + this.commandMap.set(basename, [registration]); + } + } + + private invalid = (culprit?: string) => { + console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(culprit))); + this.busy = false; + } + + private considerInput = async (line: string) => { + if (this.busy) { + console.log(red("Busy")); + return; + } + this.busy = true; + line = line.trim(); + if (this.isCaseSensitive) { + line = line.toLowerCase(); + } + const [command, ...args] = line.split(/\s+/g); + if (!command) { + return this.invalid(); + } + const registered = this.commandMap.get(command); + if (registered) { + const { length } = args; + const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); + for (const { argPatterns, action } of candidates) { + const parsed: string[] = []; + let matched = false; + if (length) { + for (let i = 0; i < length; i++) { + let matches: RegExpExecArray | null; + if ((matches = argPatterns[i].exec(args[i])) === null) { + break; + } + parsed.push(matches[0]); + } + matched = true; + } + if (!length || matched) { + await action(parsed[Symbol.iterator]()); + this.busy = false; + return; + } + } + } + this.invalid(command); + } + +} \ No newline at end of file diff --git a/src/server/session.ts b/src/server/session.ts deleted file mode 100644 index ec51b6e18..000000000 --- a/src/server/session.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { yellow, red, cyan, magenta, green } from "colors"; -import { isMaster, on, fork, setupMaster, Worker } from "cluster"; -import InputManager from "./session_manager/input_manager"; -import { execSync } from "child_process"; -import { Email } from "./session_manager/email"; -import { get } from "request-promise"; -import { WebSocket } from "./Websocket/Websocket"; -import { Utils } from "../Utils"; -import { MessageStore } from "./Message"; - -const onWindows = process.platform === "win32"; -const heartbeat = `http://localhost:1050/serverHeartbeat`; -const admin = ["samuel_wilkins@brown.edu"]; - -export namespace Session { - - export let key: string; - export const signature = "Best,\nServer Session Manager"; - let activeWorker: Worker; - let listening = false; - const masterIdentifier = `${yellow("__master__")}:`; - const workerIdentifier = `${magenta("__worker__")}:`; - - function log(message?: any, ...optionalParams: any[]) { - const identifier = isMaster ? masterIdentifier : workerIdentifier; - console.log(identifier, message, ...optionalParams); - } - - export async function distributeKey() { - key = Utils.GenerateGuid(); - const timestamp = new Date().toUTCString(); - const content = `The key for this session (started @ ${timestamp}) is ${key}.\n\n${signature}`; - return Promise.all(admin.map(recipient => Email.dispatch(recipient, "Server Termination Key", content))); - } - - function tryKillActiveWorker() { - if (activeWorker && !activeWorker.isDead()) { - activeWorker.process.kill(); - return true; - } - return false; - } - - function logLifecycleEvent(lifecycle: string) { - process.send?.({ lifecycle }); - } - - function messageHandler({ lifecycle, action }: any) { - if (action) { - console.log(`${workerIdentifier} action requested (${action})`); - switch (action) { - case "kill": - log(red("An authorized user has ended the server from the /kill route")); - tryKillActiveWorker(); - process.exit(0); - } - } else if (lifecycle) { - console.log(`${workerIdentifier} lifecycle phase (${lifecycle})`); - } - } - - async function activeExit(error: Error) { - if (!listening) { - return; - } - listening = false; - await Promise.all(admin.map(recipient => Email.dispatch(recipient, "Dash Web Server Crash", crashReport(error)))); - const { _socket } = WebSocket; - if (_socket) { - Utils.Emit(_socket, MessageStore.ConnectionTerminated, "Manual"); - } - logLifecycleEvent(red(`Crash event detected @ ${new Date().toUTCString()}`)); - logLifecycleEvent(red(error.message)); - process.exit(1); - } - - function crashReport({ name, message, stack }: Error) { - return [ - "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:", - `name:\n${name}`, - `message:\n${message}`, - `stack:\n${stack}`, - "The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.", - signature - ].join("\n\n"); - } - - export async function initialize(work: Function) { - if (isMaster) { - process.on("uncaughtException", error => { - if (error.message !== "Channel closed") { - log(red(error.message)); - if (error.stack) { - log(`\n${red(error.stack)}`); - } - } - }); - setupMaster({ silent: true }); - const spawn = () => { - tryKillActiveWorker(); - activeWorker = fork(); - activeWorker.on("message", messageHandler); - }; - spawn(); - on("exit", ({ process: { pid } }, code, signal) => { - const prompt = `Server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; - log(cyan(prompt)); - spawn(); - }); - const { registerCommand } = new InputManager({ identifier: masterIdentifier }); - registerCommand("exit", [], () => execSync(onWindows ? "taskkill /f /im node.exe" : "killall -9 node")); - registerCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] })); - registerCommand("restart", [], () => { - listening = false; - tryKillActiveWorker(); - }); - } else { - logLifecycleEvent(green("initializing...")); - process.on('uncaughtException', activeExit); - const checkHeartbeat = async () => { - await new Promise(resolve => { - setTimeout(async () => { - try { - await get(heartbeat); - if (!listening) { - logLifecycleEvent(green("listening...")); - } - listening = true; - resolve(); - } catch (error) { - await activeExit(error); - } - }, 1000 * 15); - }); - checkHeartbeat(); - }; - work(); - checkHeartbeat(); - } - } - -} \ No newline at end of file diff --git a/src/server/session_manager/config.ts b/src/server/session_manager/config.ts deleted file mode 100644 index ebbd999c6..000000000 --- a/src/server/session_manager/config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { resolve } from 'path'; -import { yellow } from "colors"; - -export const latency = 10; -export const ports = [1050, 4321]; -export const onWindows = process.platform === "win32"; -export const heartbeat = `http://localhost:1050/serverHeartbeat`; -export const recipient = "samuel_wilkins@brown.edu"; -export const { pid, platform } = process; - -/** - * Logging - */ -export const identifier = yellow("__session_manager__:"); - -/** - * Paths - */ -export const logPath = resolve(__dirname, "./logs"); -export const crashPath = resolve(logPath, "./crashes"); - -/** - * State - */ -export enum SessionState { - STARTING = "STARTING", - INITIALIZED = "INITIALIZED", - LISTENING = "LISTENING", - AUTOMATICALLY_RESTARTING = "CRASH_RESTARTING", - MANUALLY_RESTARTING = "MANUALLY_RESTARTING", - EXITING = "EXITING", - UPDATING = "UPDATING" -} \ No newline at end of file diff --git a/src/server/session_manager/email.ts b/src/server/session_manager/email.ts deleted file mode 100644 index a638644db..000000000 --- a/src/server/session_manager/email.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as nodemailer from "nodemailer"; -import { MailOptions } from "nodemailer/lib/json-transport"; - -export namespace Email { - - const smtpTransport = nodemailer.createTransport({ - service: 'Gmail', - auth: { - user: 'brownptcdash@gmail.com', - pass: 'browngfx1' - } - }); - - export async function dispatch(recipient: string, subject: string, content: string): Promise { - const mailOptions = { - to: recipient, - from: 'brownptcdash@gmail.com', - subject, - text: `Hello ${recipient.split("@")[0]},\n\n${content}` - } as MailOptions; - return new Promise(resolve => { - smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null)); - }); - } - -} \ No newline at end of file diff --git a/src/server/session_manager/input_manager.ts b/src/server/session_manager/input_manager.ts deleted file mode 100644 index 133b7144a..000000000 --- a/src/server/session_manager/input_manager.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { createInterface, Interface } from "readline"; -import { red } from "colors"; - -export interface Configuration { - identifier: string; - onInvalid?: (culprit?: string) => string | string; - isCaseSensitive?: boolean; -} - -type Action = (parsedArgs: IterableIterator) => any | Promise; -export interface Registration { - argPatterns: RegExp[]; - action: Action; -} - -export default class InputManager { - private identifier: string; - private onInvalid: ((culprit?: string) => string) | string; - private isCaseSensitive: boolean; - private commandMap = new Map(); - public interface: Interface; - private busy = false; - private keys: string | undefined; - - constructor({ identifier: prompt, onInvalid, isCaseSensitive }: Configuration) { - this.identifier = prompt; - this.onInvalid = onInvalid || this.usage; - this.isCaseSensitive = isCaseSensitive ?? true; - this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); - } - - private usage = () => { - const resolved = this.keys; - if (resolved) { - return resolved; - } - const members: string[] = []; - const keys = this.commandMap.keys(); - let next: IteratorResult; - while (!(next = keys.next()).done) { - members.push(next.value); - } - return `${this.identifier} commands: { ${members.sort().join(", ")} }`; - } - - public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: Action) => { - const existing = this.commandMap.get(basename); - const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); - const registration = { argPatterns: converted, action }; - if (existing) { - existing.push(registration); - } else { - this.commandMap.set(basename, [registration]); - } - } - - private invalid = (culprit?: string) => { - console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(culprit))); - this.busy = false; - } - - private considerInput = async (line: string) => { - if (this.busy) { - console.log(red("Busy")); - return; - } - this.busy = true; - line = line.trim(); - if (this.isCaseSensitive) { - line = line.toLowerCase(); - } - const [command, ...args] = line.split(/\s+/g); - if (!command) { - return this.invalid(); - } - const registered = this.commandMap.get(command); - if (registered) { - const { length } = args; - const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); - for (const { argPatterns, action } of candidates) { - const parsed: string[] = []; - let matched = false; - if (length) { - for (let i = 0; i < length; i++) { - let matches: RegExpExecArray | null; - if ((matches = argPatterns[i].exec(args[i])) === null) { - break; - } - parsed.push(matches[0]); - } - matched = true; - } - if (!length || matched) { - await action(parsed[Symbol.iterator]()); - this.busy = false; - return; - } - } - } - this.invalid(command); - } - -} \ No newline at end of file diff --git a/src/server/session_manager/logs/current_daemon_pid.log b/src/server/session_manager/logs/current_daemon_pid.log deleted file mode 100644 index 557e3d7c3..000000000 --- a/src/server/session_manager/logs/current_daemon_pid.log +++ /dev/null @@ -1 +0,0 @@ -26860 diff --git a/src/server/session_manager/logs/current_server_pid.log b/src/server/session_manager/logs/current_server_pid.log deleted file mode 100644 index 85fdb7ae0..000000000 --- a/src/server/session_manager/logs/current_server_pid.log +++ /dev/null @@ -1 +0,0 @@ -54649 created @ 2019-12-14T08:04:42.391Z diff --git a/src/server/session_manager/logs/current_session_manager_pid.log b/src/server/session_manager/logs/current_session_manager_pid.log deleted file mode 100644 index 75c23b35a..000000000 --- a/src/server/session_manager/logs/current_session_manager_pid.log +++ /dev/null @@ -1 +0,0 @@ -54643 diff --git a/src/server/session_manager/session_manager.ts b/src/server/session_manager/session_manager.ts deleted file mode 100644 index 97c2ab214..000000000 --- a/src/server/session_manager/session_manager.ts +++ /dev/null @@ -1,206 +0,0 @@ -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, latency, SessionState } from "./config"; -const killport = require("kill-port"); -import * as io from "socket.io"; - -process.on('SIGINT', endPrevious); -let state: SessionState = SessionState.STARTING; -const is = (...reference: SessionState[]) => reference.includes(state); -const set = (reference: SessionState) => state = reference; - -const endpoint = io(); -endpoint.on("connection", socket => { - -}); -endpoint.listen(process.env.PORT); - -const { registerCommand } = new InputManager({ identifier }); - -registerCommand("restart", [], async () => { - set(SessionState.MANUALLY_RESTARTING); - identifiedLog(cyan("Initializing manual restart...")); - await endPrevious(); -}); - -registerCommand("exit", [], exit); - -async function exit() { - set(SessionState.EXITING); - identifiedLog(cyan("Initializing session end")); - await endPrevious(); - identifiedLog("Cleanup complete. Exiting session...\n"); - execSync(killAllCommand()); -} - -registerCommand("update", [], async () => { - set(SessionState.UPDATING); - identifiedLog(cyan("Initializing server update from version control...")); - await endPrevious(); - await new Promise(resolve => { - exec(updateCommand(), error => { - if (error) { - identifiedLog(red(error.message)); - } - resolve(); - }); - }); - await exit(); -}); - -registerCommand("state", [], () => identifiedLog(state)); - -if (!existsSync(logPath)) { - mkdirSync(logPath); -} -if (!existsSync(crashPath)) { - mkdirSync(crashPath); -} - -function addLogEntry(message: string, color: Color) { - const formatted = color(`${message} ${timestamp()}.`); - identifiedLog(formatted); - // appendFileSync(resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`), `${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); -} - -const windowsPrepend = (command: string) => `"C:\\Program Files\\Git\\git-bash.exe" -c "${command}"`; -const macPrepend = (command: string) => `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && ${command}"\nend tell'`; - -function updateCommand() { - const command = "git pull && npm install"; - if (onWindows) { - return windowsPrepend(command); - } - return macPrepend(command); -} - -function startServerCommand() { - const command = "npm run start-release"; - if (onWindows) { - return windowsPrepend(command); - } - return macPrepend(command); -} - -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(); - 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() { - const listening = is(SessionState.LISTENING); - let error: any; - try { - listening && process.stdout.write(`${identifier} 👂 `); - await request.get(heartbeat); - listening && console.log('⇠ 💚'); - if (!listening) { - addLogEntry(is(SessionState.INITIALIZED) ? "Server successfully started" : "Backup server successfully restarted", green); - set(SessionState.LISTENING); - } - } catch (e) { - listening && console.log("⇠ 💔\n"); - error = e; - } finally { - if (error && !is(SessionState.AUTOMATICALLY_RESTARTING, SessionState.INITIALIZED, SessionState.UPDATING)) { - if (is(SessionState.STARTING)) { - set(SessionState.INITIALIZED); - } else if (is(SessionState.MANUALLY_RESTARTING)) { - set(SessionState.AUTOMATICALLY_RESTARTING); - } else { - set(SessionState.AUTOMATICALLY_RESTARTING); - 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...")); - } - current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || is(SessionState.INITIALIZED) ? "Spawned initial server." : "Previous server process exited.")); - writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`); - } - setTimeout(checkHeartbeat, 1000 * latency); - } -} - -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: ${heartbeat}\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(resolve => { - smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null)); - }); -} - -identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`)); -checkHeartbeat(); \ No newline at end of file diff --git a/src/server/session_manager/session_manager_cluster.ts b/src/server/session_manager/session_manager_cluster.ts deleted file mode 100644 index 546465c03..000000000 --- a/src/server/session_manager/session_manager_cluster.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { isMaster, fork, on } from "cluster"; -import { cpus } from "os"; -import { createServer } from "http"; - -const capacity = cpus().length; - -let thrown = false; - -if (isMaster) { - console.log(capacity); - for (let i = 0; i < capacity; i++) { - fork(); - } - on("exit", (worker, code, signal) => { - console.log(`worker ${worker.process.pid} died with code ${code} and signal ${signal}`); - fork(); - }); -} else { - const port = 1234; - createServer().listen(port, () => { - console.log('process id local', process.pid); - console.log(`http server started at port ${port}`); - if (!thrown) { - thrown = true; - setTimeout(() => { - throw new Error("Hey I'm a fake error!"); - }, 1000); - } - }); -} - -process.on('uncaughtException', function (err) { - console.error((new Date).toUTCString() + ' uncaughtException:', err.message); - console.error(err.stack); - process.exit(1); -}); \ No newline at end of file -- cgit v1.2.3-70-g09d2