aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/server/ActionUtilities.ts42
-rw-r--r--src/server/index.ts29
-rw-r--r--src/server/persistence_daemon.ts79
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