aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--session.config.json14
-rw-r--r--src/server/Session/session.ts124
-rw-r--r--src/server/Session/session_config_schema.ts46
-rw-r--r--src/server/index.ts1
-rw-r--r--src/server/repl.ts1
5 files changed, 132 insertions, 54 deletions
diff --git a/session.config.json b/session.config.json
index ebe17763e..da721efa5 100644
--- a/session.config.json
+++ b/session.config.json
@@ -1,15 +1,15 @@
{
"showServerOutput": false,
- "recipients": [
- "samuel_wilkins@brown.edu"
- ],
"ports": {
"server": 1050,
"socket": 4321
},
+ "email": {
+ "recipients": [
+ "samuel_wilkins@brown.edu"
+ ],
+ "signature": "Best,\nDash Server Session Manager"
+ },
"heartbeatRoute": "/serverHeartbeat",
- "pollingIntervalSeconds": 15,
- "masterIdentifier": "__master__",
- "workerIdentifier": "__worker__",
- "signature": "Best,\nServer Session Manager"
+ "pollingIntervalSeconds": 15
} \ No newline at end of file
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts
index 61b8bcf16..15f7e8292 100644
--- a/src/server/Session/session.ts
+++ b/src/server/Session/session.ts
@@ -24,6 +24,34 @@ const onWindows = process.platform === "win32";
*/
export namespace Session {
+ interface EmailOptions {
+ recipients: string[];
+ signature?: string;
+ }
+
+ interface Configuration {
+ showServerOutput: boolean;
+ masterIdentifier: string;
+ workerIdentifier: string;
+ email: EmailOptions | undefined;
+ ports: { [description: string]: number };
+ heartbeatRoute: string;
+ pollingIntervalSeconds: number;
+ [key: string]: any;
+ }
+
+ const defaultConfiguration: Configuration = {
+ showServerOutput: false,
+ masterIdentifier: yellow("__monitor__:"),
+ workerIdentifier: magenta("__server__:"),
+ email: undefined,
+ ports: { server: 3000 },
+ heartbeatRoute: "/",
+ pollingIntervalSeconds: 30
+ };
+
+ const defaultSignature = "-Server Session Manager";
+
interface MasterCustomizer {
addReplCommand: (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => void;
addChildMessageHandler: (message: string, handler: ActionHandler) => void;
@@ -42,65 +70,98 @@ export namespace Session {
}
export type CrashEmailGenerator = (error: Error) => EmailTemplate | Promise<EmailTemplate>;
+ function defaultEmailGenerator({ name, message, stack }: Error) {
+ return {
+ subject: "Server Crash Event",
+ body: [
+ "You 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 automatically"
+ ].join("\n\n")
+ };
+ }
+
/**
* Validates and reads the configuration file, accordingly builds a child process factory
* and spawns off an initial process that will respawn as predecessors die.
*/
- export async function initializeMonitorThread(crashEmailGenerator?: CrashEmailGenerator): Promise<MasterCustomizer> {
+ export async function initializeMonitorThread(custom?: CrashEmailGenerator): Promise<MasterCustomizer> {
let activeWorker: Worker;
const childMessageHandlers: { [message: string]: (action: SessionAction, args: any) => void } = {};
+ const crashEmailGenerator = custom || defaultEmailGenerator;
// read in configuration .json file only once, in the master thread
// pass down any variables the pertinent to the child processes as environment variables
- const configuration = function loadConfiguration(): any {
+ const {
+ masterIdentifier,
+ workerIdentifier,
+ ports,
+ email,
+ heartbeatRoute,
+ showServerOutput,
+ pollingIntervalSeconds
+ } = function loadAndValidateConfiguration(): any {
try {
- const configuration = JSON.parse(readFileSync('./session.config.json', 'utf8'));
+ const configuration: Configuration = JSON.parse(readFileSync('./session.config.json', 'utf8'));
const options = {
throwError: true,
allowUnknownAttributes: false
};
// ensure all necessary and no excess information is specified by the configuration file
validate(configuration, configurationSchema, options);
- configuration.masterIdentifier = yellow(configuration.masterIdentifier + ":");
- configuration.workerIdentifier = magenta(configuration.workerIdentifier + ":");
+ let formatMaster = true;
+ let formatWorker = true;
+ Object.keys(defaultConfiguration).forEach(property => {
+ if (!configuration[property]) {
+ if (property === "masterIdentifier") {
+ formatMaster = false;
+ } else if (property === "workerIdentifier") {
+ formatWorker = false;
+ }
+ configuration[property] = defaultConfiguration[property];
+ }
+ });
+ if (formatMaster) {
+ configuration.masterIdentifier = yellow(configuration.masterIdentifier + ":");
+ }
+ if (formatWorker) {
+ configuration.workerIdentifier = magenta(configuration.workerIdentifier + ":");
+ }
return configuration;
} catch (error) {
- console.log(red("\nSession configuration failed."));
if (error instanceof ValidationError) {
+ console.log(red("\nSession configuration failed."));
console.log("The given session.config.json configuration file is invalid.");
console.log(`${error.instance}: ${error.stack}`);
+ process.exit(0);
} else if (error.code === "ENOENT" && error.path === "./session.config.json") {
- console.log("Please include a session.config.json configuration file in your project root.");
+ console.log(defaultConfiguration.masterIdentifier, "consider including a session.config.json configuration file in your project root.");
+ return defaultConfiguration;
} else {
+ console.log(red("\nSession configuration failed."));
console.log("The following unknown error occurred during configuration.");
console.log(error.stack);
+ process.exit(0);
}
- console.log();
- process.exit(0);
}
}();
- const {
- masterIdentifier,
- workerIdentifier,
- recipients,
- ports,
- signature,
- heartbeatRoute,
- showServerOutput,
- pollingIntervalSeconds
- } = configuration;
-
// this sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
// to kill the server via the /kill/:key route
- const key = Utils.GenerateGuid();
- const timestamp = new Date().toUTCString();
- const content = `The key for this session (started @ ${timestamp}) is ${key}.\n\n${signature}`;
- const results = await Email.dispatchAll(recipients, "Server Termination Key", content);
- if (results.some(success => !success)) {
- console.log(masterIdentifier, red("distribution of session key failed"));
- } else {
- console.log(masterIdentifier, green("distributed session key to recipients"));
+ let key: string | undefined;
+ if (email) {
+ const { recipients, signature } = email;
+ key = Utils.GenerateGuid();
+ const timestamp = new Date().toUTCString();
+ const content = `The key for this session (started @ ${timestamp}) is ${key}.\n\n${signature || defaultSignature}`;
+ const results = await Email.dispatchAll(recipients, "Server Termination Key", content);
+ if (results.some(success => !success)) {
+ console.log(masterIdentifier, red("distribution of session key failed"));
+ } else {
+ console.log(masterIdentifier, green("distributed session key to recipients"));
+ }
}
// handle exceptions in the master thread - there shouldn't be many of these
@@ -167,10 +228,11 @@ export namespace Session {
tryKillActiveWorker(false);
process.exit(0);
case "notify_crash":
- if (crashEmailGenerator) {
+ if (email) {
+ const { recipients, signature } = email;
const { error } = args;
const { subject, body } = await crashEmailGenerator(error);
- const content = `${body}\n\n${signature}`;
+ const content = `${body}\n\n${signature || defaultSignature}`;
Email.dispatchAll(recipients, subject, content);
}
case "set_port":
@@ -224,7 +286,7 @@ export namespace Session {
const exitHandlers: ExitHandler[] = [];
// notify master thread (which will log update in the console) of initialization via IPC
- process.send?.({ lifecycle: green("initializing...") });
+ process.send?.({ lifecycle: green("compiling and initializing...") });
// updates the local value of listening to the value sent from master
process.on("message", ({ setListening }) => listening = setListening);
diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts
index 34d1ad523..0acb304db 100644
--- a/src/server/Session/session_config_schema.ts
+++ b/src/server/Session/session_config_schema.ts
@@ -1,17 +1,9 @@
import { Schema } from "jsonschema";
const emailPattern = /^(([a-zA-Z0-9_.-])+@([a-zA-Z0-9_.-])+\.([a-zA-Z])+([a-zA-Z])+)?$/g;
-const localPortPattern = /\/[a-zA-Z]+/g;
+const localPortPattern = /\/[a-zA-Z]*/g;
const properties: { [name: string]: Schema } = {
- recipients: {
- type: "array",
- items: {
- type: "string",
- pattern: emailPattern
- },
- minLength: 1
- },
ports: {
type: "object",
properties: {
@@ -25,16 +17,42 @@ const properties: { [name: string]: Schema } = {
type: "string",
pattern: localPortPattern
},
- signature: { type: "string" },
- masterIdentifier: { type: "string", minLength: 1 },
- workerIdentifier: { type: "string", minLength: 1 },
+ email: {
+ type: "object",
+ properties: {
+ recipients: {
+ type: "array",
+ items: {
+ type: "string",
+ pattern: emailPattern
+ },
+ minLength: 1
+ },
+ signature: {
+ type: "string",
+ minLength: 1
+ }
+ },
+ required: ["recipients"]
+ },
+ masterIdentifier: {
+ type: "string",
+ minLength: 1
+ },
+ workerIdentifier: {
+ type: "string",
+ minLength: 1
+ },
showServerOutput: { type: "boolean" },
- pollingIntervalSeconds: { type: "number", minimum: 1, maximum: 86400 }
+ pollingIntervalSeconds: {
+ type: "number",
+ minimum: 1,
+ maximum: 86400
+ }
};
export const configurationSchema: Schema = {
id: "/configuration",
type: "object",
properties,
- required: Object.keys(properties)
}; \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 0fee41bd8..3dc0c739e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -96,7 +96,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
secureHandler: ({ req, res }) => {
if (req.params.key === process.env.session_key) {
res.send("<img src='https://media.giphy.com/media/NGIfqtcS81qi4/giphy.gif' style='width:100%;height:100%;'/>");
- // setTimeout(() => process.send!({ action: { message: "kill" } }), 1000 * 5);
process.send!({ action: { message: "kill" } });
} else {
res.redirect("/home");
diff --git a/src/server/repl.ts b/src/server/repl.ts
index a47d4aad4..b77fbcefc 100644
--- a/src/server/repl.ts
+++ b/src/server/repl.ts
@@ -70,7 +70,6 @@ export default class Repl {
}
private considerInput = async (line: string) => {
- console.log("raw", line);
if (this.busy) {
console.log(red("Busy"));
return;