diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/server/ActionUtilities.ts | 42 | ||||
-rw-r--r-- | src/server/index.ts | 29 | ||||
-rw-r--r-- | src/server/persistence_daemon.ts | 79 |
3 files changed, 134 insertions, 16 deletions
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index 4667254d8..53ddea2fc 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -5,11 +5,13 @@ import * as path from 'path'; import * as rimraf from "rimraf"; import { yellow, Color } from 'colors'; +const projectRoot = path.resolve(__dirname, "../../"); + export const command_line = (command: string, fromDirectory?: string) => { return new Promise<string>((resolve, reject) => { const options: ExecOptions = {}; if (fromDirectory) { - options.cwd = path.resolve(__dirname, fromDirectory); + options.cwd = fromDirectory ? path.resolve(projectRoot, fromDirectory) : projectRoot; } exec(command, options, (err, stdout) => err ? reject(err) : resolve(stdout)); }); @@ -29,31 +31,43 @@ export const write_text_file = (relativePath: string, contents: any) => { }); }; +export type Messager<T> = (outcome: { result: T | undefined, error: Error | null }) => string; + export interface LogData<T> { startMessage: string; - endMessage: string; + // if you care about the execution informing your log, you can pass in a function that takes in the result and a potential error and decides what to write + endMessage: string | Messager<T>; action: () => T | Promise<T>; color?: Color; } let current = Math.ceil(Math.random() * 20); -export async function log_execution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T> { - let result: T; - const formattedStart = `${startMessage}...`; - const formattedEnd = `${endMessage}.`; - if (color) { - console.log(color(formattedStart)); +export async function log_execution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T | undefined> { + let result: T | undefined = undefined, error: Error | null = null; + const resolvedColor = color || `\x1b[${31 + ++current % 6}m%s\x1b[0m`; + log_helper(`${startMessage}...`, resolvedColor); + try { result = await action(); - console.log(color(formattedEnd)); - } else { - const color = `\x1b[${31 + current++ % 6}m%s\x1b[0m`; - console.log(color, formattedStart); - result = await action(); - console.log(color, formattedEnd); + } catch (e) { + error = e; + } finally { + if (typeof endMessage === "string") { + log_helper(`${endMessage}.`, resolvedColor); + } else { + log_helper(`${endMessage({ result, error })}.`, resolvedColor); + } } return result; } +function log_helper(content: string, color: Color | string) { + if (typeof color === "string") { + console.log(color, content); + } else { + console.log(color(content)); + } +} + export function logPort(listener: string, port: number) { console.log(`${listener} listening on port ${yellow(String(port))}`); } diff --git a/src/server/index.ts b/src/server/index.ts index 6099af83c..3764eaabb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -18,10 +18,10 @@ import { GoogleCredentialsLoader } from './credentials/CredentialsLoader'; import DeleteManager from "./ApiManagers/DeleteManager"; import PDFManager from "./ApiManagers/PDFManager"; import UploadManager from "./ApiManagers/UploadManager"; -import { log_execution } from "./ActionUtilities"; +import { log_execution, command_line } from "./ActionUtilities"; import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; -import { yellow } from "colors"; +import { yellow, red } from "colors"; import { disconnect } from "../server/Initialization"; export const publicDirectory = path.resolve(__dirname, "public"); @@ -119,6 +119,31 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: } }); + let daemonInitialized = false; + addSupervisedRoute({ + method: Method.GET, + subscription: "/persist", + onValidation: async ({ res }) => { + if (!daemonInitialized) { + daemonInitialized = true; + log_execution({ + startMessage: "\ninitializing persistence daemon", + endMessage: ({ result, error }) => { + const success = error === null && result !== undefined; + if (!success) { + console.log(red("failed to initialize the persistance daemon")); + process.exit(0); + } + return "persistence daemon process closed"; + }, + action: async () => command_line("npx ts-node ./persistence_daemon.ts", "./src/server"), + color: yellow + }); + } + res.redirect("/home"); + } + }); + logRegistrationOutcome(); // initialize the web socket (bidirectional communication: if a user changes diff --git a/src/server/persistence_daemon.ts b/src/server/persistence_daemon.ts new file mode 100644 index 000000000..388440b49 --- /dev/null +++ b/src/server/persistence_daemon.ts @@ -0,0 +1,79 @@ +import * as request from "request-promise"; +import { command_line, log_execution } from "./ActionUtilities"; +import { red, yellow, cyan, green } from "colors"; +import * as nodemailer from "nodemailer"; +import { MailOptions } from "nodemailer/lib/json-transport"; + +const { LOCATION } = process.env; +const recipient = "samuel_wilkins@brown.edu"; +let restarting = false; + +async function listen() { + if (!LOCATION) { + console.log(red("No location specified for persistence daemon. Please include as a command line environment variable or in a .env file.")); + process.exit(0); + } + const heartbeat = `${LOCATION}:1050/serverHeartbeat`; + // if this is on our remote server, the server must be run in release mode + const suffix = LOCATION.includes("localhost") ? "" : "-release"; + setInterval(async () => { + let response: any; + let error: any; + try { + response = await request.get(heartbeat); + } catch (e) { + error = e; + } finally { + if (!response && !restarting) { + restarting = true; + console.log(yellow("Detected a server crash!")); + await log_execution({ + startMessage: "Sending crash notification email", + endMessage: ({ error, result }) => { + const success = error === null && result === true; + return (success ? `Notification successfully sent to ` : `Failed to notify `) + recipient; + }, + action: async () => notify(error || "Hmm, no error to report..."), + color: cyan + }); + console.log(await log_execution({ + startMessage: "Initiating server restart", + endMessage: "Server successfully restarted", + action: async () => command_line(`npm run start${suffix}`, "../../"), + color: green + })); + restarting = false; + } + } + }, 1000 * 90); +} + +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)); + }); +} + +listen();
\ No newline at end of file |