aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/CollectionMulticolumnView.tsx44
-rw-r--r--src/server/ApiManagers/SessionManager.ts6
-rw-r--r--src/server/DashSession/DashSessionAgent.ts220
-rw-r--r--src/server/DashSession/templates/crash_instructions.txt14
-rw-r--r--src/server/DashSession/templates/remote_debug_instructions.txt (renamed from src/server/remote_debug_instructions.txt)0
-rw-r--r--src/server/DashSessionAgent.ts168
-rw-r--r--src/server/index.ts2
-rw-r--r--src/server/session/agents/applied_session_agent.ts9
-rw-r--r--src/server/session/agents/monitor.ts84
-rw-r--r--src/server/session/agents/server_worker.ts4
-rw-r--r--src/server/session/utilities/repl.ts (renamed from src/server/repl.ts)2
11 files changed, 330 insertions, 223 deletions
diff --git a/src/client/views/CollectionMulticolumnView.tsx b/src/client/views/CollectionMulticolumnView.tsx
new file mode 100644
index 000000000..94e86c048
--- /dev/null
+++ b/src/client/views/CollectionMulticolumnView.tsx
@@ -0,0 +1,44 @@
+import { observer } from 'mobx-react';
+import { makeInterface } from '../../new_fields/Schema';
+import { documentSchema } from '../../new_fields/documentSchemas';
+import { CollectionSubView, SubCollectionViewProps } from './collections/CollectionSubView';
+import { DragManager } from '../util/DragManager';
+import * as React from "react";
+import { Doc } from '../../new_fields/Doc';
+import { NumCast } from '../../new_fields/Types';
+
+type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
+const MulticolumnDocument = makeInterface(documentSchema);
+
+@observer
+export default class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
+
+ constructor(props: Readonly<SubCollectionViewProps>) {
+ super(props);
+ const { Document } = this.props;
+ Document.multicolumnData = new Doc();
+ }
+
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ }
+ }
+
+ public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+
+ render() {
+ return (
+ <div className={"collectionMulticolumnView_outer"}>
+ <div className={"collectionMulticolumnView_contents"}>
+ {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(({ layout, data }) => {
+
+ })}
+ </div>
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index 6782643bc..21103fdd5 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -2,6 +2,7 @@ import ApiManager, { Registration } from "./ApiManager";
import { Method, _permission_denied, AuthorizedCore, SecureHandler } from "../RouteManager";
import RouteSubscriber from "../RouteSubscriber";
import { sessionAgent } from "..";
+import { DashSessionAgent } from "../DashSession/DashSessionAgent";
const permissionError = "You are not authorized!";
@@ -27,10 +28,11 @@ export default class SessionManager extends ApiManager {
register({
method: Method.GET,
- subscription: this.secureSubscriber("debug", "mode", "recipient"),
+ subscription: this.secureSubscriber("debug", "mode", "recipient?"),
secureHandler: this.authorizedAction(async ({ req, res }) => {
- const { mode, recipient } = req.params;
+ const { mode } = req.params;
if (["passive", "active"].includes(mode)) {
+ const recipient = req.params.recipient || DashSessionAgent.notificationRecipient;
const response = await sessionAgent.serverWorker.sendMonitorAction("debug", { mode, recipient }, true);
if (response instanceof Error) {
res.send(response);
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
new file mode 100644
index 000000000..f3f0a3c3d
--- /dev/null
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -0,0 +1,220 @@
+import { Email, pathFromRoot } from "../ActionUtilities";
+import { red, yellow, green, cyan } from "colors";
+import { get } from "request-promise";
+import { Utils } from "../../Utils";
+import { WebSocket } from "../Websocket/Websocket";
+import { MessageStore } from "../Message";
+import { launchServer, onWindows } from "..";
+import { existsSync, mkdirSync, readdirSync, statSync, createWriteStream, readFileSync } from "fs";
+import * as Archiver from "archiver";
+import { resolve } from "path";
+import { AppliedSessionAgent, ExitHandler } from "../session/agents/applied_session_agent";
+import { Monitor } from "../session/agents/monitor";
+import { ServerWorker } from "../session/agents/server_worker";
+
+/**
+ * If we're the monitor (master) thread, we should launch the monitor logic for the session.
+ * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
+ * our job should be to run the server.
+ */
+export class DashSessionAgent extends AppliedSessionAgent {
+
+ private readonly signature = "-Dash Server Session Manager";
+ private readonly releaseDesktop = pathFromRoot("../../Desktop");
+
+ /**
+ * The core method invoked when the single master thread is initialized.
+ * Installs event hooks, repl commands and additional IPC listeners.
+ */
+ protected async initializeMonitor(monitor: Monitor) {
+ monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
+ monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
+ monitor.addReplCommand("backup", [], this.backup);
+ monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async ([mode, recipient]) => this.dispatchZippedDebugBackup(mode, recipient));
+ monitor.addServerMessageListener("backup", this.backup);
+ monitor.addServerMessageListener("debug", ({ args: { mode, recipient } }) => this.dispatchZippedDebugBackup(mode, recipient));
+ monitor.on(Monitor.IntrinsicEvents.KeyGenerated, this.dispatchSessionPassword);
+ monitor.on(Monitor.IntrinsicEvents.CrashDetected, this.dispatchCrashReport);
+ }
+
+ /**
+ * The core method invoked when a server worker thread is initialized.
+ * Installs logic to be executed when the server worker dies.
+ */
+ protected async initializeServerWorker() {
+ const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
+ worker.addExitHandler(this.notifyClient);
+ return worker;
+ }
+
+ /**
+ * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
+ */
+ private _remoteDebugInstructions: string | undefined;
+ private generateDebugInstructions = (zipName: string, target: string) => {
+ if (!this._remoteDebugInstructions) {
+ this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._remoteDebugInstructions
+ .replace(/__zipname__/, zipName)
+ .replace(/__target__/, target)
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * Prepares the body of the email with information regarding a crash event.
+ */
+ private _crashInstructions: string | undefined;
+ private generateCrashInstructions({ name, message, stack }: Error) {
+ if (!this._crashInstructions) {
+ this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._crashInstructions
+ .replace(/__name__/, name || "[no error name found]")
+ .replace(/__message__/, message || "[no error message found]")
+ .replace(/__stack__/, stack || "[no error stack found]")
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
+ * to kill the server via the /kill/:key route.
+ */
+ private dispatchSessionPassword = async (key: string) => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ mainLog(green("dispatching session key..."));
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Release Session Admin Authentication Key",
+ content: `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ mainLog(red("distribution of session key experienced errors"));
+ } else {
+ mainLog(green("successfully distributed session key to recipients"));
+ }
+ }
+
+ /**
+ * This sends an email with the generated crash report.
+ */
+ private dispatchCrashReport = async (crashCause: Error) => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Web Server Crash",
+ content: this.generateCrashInstructions(crashCause)
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ mainLog(red("distribution of crash notification experienced errors"));
+ } else {
+ mainLog(green("successfully distributed crash notification to recipients"));
+ }
+ }
+
+ /**
+ * Logic for interfacing with Solr. Either starts it,
+ * stops it, or rebuilds its indicies.
+ */
+ private executeSolrCommand = async (args: string[]) => {
+ const { exec, mainLog } = this.sessionMonitor;
+ const action = args[0];
+ if (action === "index") {
+ exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
+ } else {
+ const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
+ await exec(command, { cwd: "./solr-8.3.1/bin" });
+ try {
+ await get("http://localhost:8983");
+ mainLog(green("successfully connected to 8983 after running solr initialization"));
+ } catch {
+ mainLog(red("unable to connect at 8983 after running solr initialization"));
+ }
+ }
+ }
+
+ /**
+ * Broadcast to all clients that their connection
+ * is no longer valid, and explain why / what to expect.
+ */
+ private notifyClient: ExitHandler = reason => {
+ const { _socket } = WebSocket;
+ if (_socket) {
+ const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
+ Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+ }
+ }
+
+ /**
+ * Performs a backup of the database, saved to the desktop subdirectory.
+ * This should work as is only on our specific release server.
+ */
+ private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+
+ /**
+ * Compress either a brand new backup or the most recent backup and send it
+ * as an attachment to an email, dispatched to the requested recipient.
+ * @param mode specifies whether or not to make a new backup before exporting
+ * @param to the recipient of the email
+ */
+ private async dispatchZippedDebugBackup(mode: string, to: string) {
+ const { mainLog } = this.sessionMonitor;
+ try {
+ // if desired, complete an immediate backup to send
+ if (mode === "active") {
+ await this.backup();
+ mainLog("backup complete");
+ }
+
+ // ensure the directory for compressed backups exists
+ const backupsDirectory = `${this.releaseDesktop}/backups`;
+ const compressedDirectory = `${this.releaseDesktop}/compressed`;
+ if (!existsSync(compressedDirectory)) {
+ mkdirSync(compressedDirectory);
+ }
+
+ // sort all backups by their modified time, and choose the most recent one
+ const target = readdirSync(backupsDirectory).map(filename => ({
+ modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
+ filename
+ })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
+ mainLog(`targeting ${target}...`);
+
+ // create a zip file and to it, write the contents of the backup directory
+ const zipName = `${target}.zip`;
+ const zipPath = `${compressedDirectory}/${zipName}`;
+ const output = createWriteStream(zipPath);
+ const zip = Archiver('zip');
+ zip.pipe(output);
+ zip.directory(`${backupsDirectory}/${target}/Dash`, false);
+ await zip.finalize();
+ mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
+
+ // dispatch the email to the recipient, containing the finalized zip file
+ const error = await Email.dispatch({
+ to,
+ subject: `Remote debug: compressed backup of ${target}...`,
+ content: this.generateDebugInstructions(zipName, target),
+ attachments: [{ filename: zipName, path: zipPath }]
+ });
+
+ // indicate success or failure
+ mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
+ error && mainLog(red(error.message));
+ } catch (error) {
+ mainLog(red("unable to dispatch zipped backup..."));
+ mainLog(red(error.message));
+ }
+ }
+
+}
+
+export namespace DashSessionAgent {
+
+ export const notificationRecipient = "brownptcdash@gmail.com";
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/templates/crash_instructions.txt b/src/server/DashSession/templates/crash_instructions.txt
new file mode 100644
index 000000000..65417919d
--- /dev/null
+++ b/src/server/DashSession/templates/crash_instructions.txt
@@ -0,0 +1,14 @@
+You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:
+
+name:
+__name__
+
+message:
+__message__
+
+stack:
+__stack__
+
+The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.
+
+__signature__ \ No newline at end of file
diff --git a/src/server/remote_debug_instructions.txt b/src/server/DashSession/templates/remote_debug_instructions.txt
index c279c460a..c279c460a 100644
--- a/src/server/remote_debug_instructions.txt
+++ b/src/server/DashSession/templates/remote_debug_instructions.txt
diff --git a/src/server/DashSessionAgent.ts b/src/server/DashSessionAgent.ts
deleted file mode 100644
index 3073e69c3..000000000
--- a/src/server/DashSessionAgent.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { Email, pathFromRoot } from "./ActionUtilities";
-import { red, yellow, green, cyan } from "colors";
-import { get } from "request-promise";
-import { Utils } from "../Utils";
-import { WebSocket } from "./Websocket/Websocket";
-import { MessageStore } from "./Message";
-import { launchServer, onWindows } from ".";
-import { existsSync, mkdirSync, readdirSync, statSync, createWriteStream, readFileSync } from "fs";
-import * as Archiver from "archiver";
-import { resolve } from "path";
-import { AppliedSessionAgent, ExitHandler } from "./session/agents/applied_session_agent";
-import { Monitor } from "./session/agents/monitor";
-import { ServerWorker } from "./session/agents/server_worker";
-
-/**
- * If we're the monitor (master) thread, we should launch the monitor logic for the session.
- * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
- * our job should be to run the server.
- */
-export class DashSessionAgent extends AppliedSessionAgent {
-
- private readonly notificationRecipients = ["samuel_wilkins@brown.edu"];
- private readonly signature = "-Dash Server Session Manager";
- private readonly releaseDesktop = pathFromRoot("../../Desktop");
- private _instructions: string | undefined;
- private get instructions() {
- if (!this._instructions) {
- this._instructions = readFileSync(resolve(__dirname, "./remote_debug_instructions.txt"), { encoding: "utf8" });
- }
- return this._instructions;
- }
-
- protected async launchMonitor() {
- const monitor = Monitor.Create(this.notifiers);
- monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
- monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
- monitor.addReplCommand("backup", [], this.backup);
- monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async ([mode, recipient]) => this.dispatchZippedDebugBackup(mode, recipient));
- monitor.addServerMessageListener("backup", this.backup);
- monitor.addServerMessageListener("debug", ({ args: { mode, recipient } }) => this.dispatchZippedDebugBackup(mode, recipient));
- return monitor;
- }
-
- protected async launchServerWorker() {
- const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
- worker.addExitHandler(this.notifyClient);
- return worker;
- }
-
- private readonly notifiers: Monitor.NotifierHooks = {
- key: async key => {
- // this sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
- // to kill the server via the /kill/:key route
- const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`;
- const failures = await Email.dispatchAll({
- to: this.notificationRecipients,
- subject: "Dash Release Session Admin Authentication Key",
- content
- });
- if (failures) {
- failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- },
- crash: async ({ name, message, stack }) => {
- const body = [
- "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.",
- ].join("\n\n");
- const content = `${body}\n\n${this.signature}`;
- const failures = await Email.dispatchAll({
- to: this.notificationRecipients,
- subject: "Dash Web Server Crash",
- content
- });
- if (failures) {
- failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- }
- };
-
- private executeSolrCommand = async (args: string[]) => {
- const { exec, mainLog } = this.sessionMonitor;
- const action = args[0];
- if (action === "index") {
- exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
- } else {
- const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
- await exec(command, { cwd: "./solr-8.3.1/bin" });
- try {
- await get("http://localhost:8983");
- mainLog(green("successfully connected to 8983 after running solr initialization"));
- } catch {
- mainLog(red("unable to connect at 8983 after running solr initialization"));
- }
- }
- }
-
- private notifyClient: ExitHandler = reason => {
- const { _socket } = WebSocket;
- if (_socket) {
- const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
- Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
- }
- }
-
- private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
-
- private async dispatchZippedDebugBackup(mode: string, to: string) {
- const { mainLog } = this.sessionMonitor;
- try {
- // if desired, complete an immediate backup to send
- if (mode === "active") {
- await this.backup();
- mainLog("backup complete");
- }
-
- // ensure the directory for compressed backups exists
- const backupsDirectory = `${this.releaseDesktop}/backups`;
- const compressedDirectory = `${this.releaseDesktop}/compressed`;
- if (!existsSync(compressedDirectory)) {
- mkdirSync(compressedDirectory);
- }
-
- // sort all backups by their modified time, and choose the most recent one
- const target = readdirSync(backupsDirectory).map(filename => ({
- modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
- filename
- })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
- mainLog(`targeting ${target}...`);
-
- // create a zip file and to it, write the contents of the backup directory
- const zipName = `${target}.zip`;
- const zipPath = `${compressedDirectory}/${zipName}`;
- const output = createWriteStream(zipPath);
- const zip = Archiver('zip');
- zip.pipe(output);
- zip.directory(`${backupsDirectory}/${target}/Dash`, false);
- await zip.finalize();
- mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
-
- // dispatch the email to the recipient, containing the finalized zip file
- const error = await Email.dispatch({
- to,
- subject: `Remote debug: compressed backup of ${target}...`,
- content: this.instructions // prepare the body of the email with instructions on restoring the local database
- .replace(/__zipname__/, zipName)
- .replace(/__target__/, target)
- .replace(/__signature__/, this.signature),
- attachments: [{ filename: zipName, path: zipPath }]
- });
-
- // indicate success or failure
- mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
- error && mainLog(red(error.message));
- } catch (error) {
- mainLog(red("unable to dispatch zipped backup..."));
- mainLog(red(error.message));
- }
- }
-
-} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index de56d31bf..0cce0dc54 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -22,7 +22,7 @@ import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { Logger } from "./ProcessFactory";
import { yellow } from "colors";
-import { DashSessionAgent } from "./DashSessionAgent";
+import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
import { AppliedSessionAgent } from "./session/agents/applied_session_agent";
diff --git a/src/server/session/agents/applied_session_agent.ts b/src/server/session/agents/applied_session_agent.ts
index cb7f63c34..53293d3bf 100644
--- a/src/server/session/agents/applied_session_agent.ts
+++ b/src/server/session/agents/applied_session_agent.ts
@@ -8,8 +8,8 @@ export abstract class AppliedSessionAgent {
// the following two methods allow the developer to create a custom
// session and use the built in customization options for each thread
- protected abstract async launchMonitor(): Promise<Monitor>;
- protected abstract async launchServerWorker(): Promise<ServerWorker>;
+ protected abstract async initializeMonitor(monitor: Monitor): Promise<void>;
+ protected abstract async initializeServerWorker(): Promise<ServerWorker>;
private launched = false;
@@ -43,9 +43,10 @@ export abstract class AppliedSessionAgent {
if (!this.launched) {
this.launched = true;
if (isMaster) {
- this.sessionMonitorRef = await this.launchMonitor();
+ await this.initializeMonitor(this.sessionMonitorRef = Monitor.Create());
+ this.sessionMonitorRef.finalize();
} else {
- this.serverWorkerRef = await this.launchServerWorker();
+ this.serverWorkerRef = await this.initializeServerWorker();
}
} else {
throw new Error("Cannot launch a session thread more than once per process.");
diff --git a/src/server/session/agents/monitor.ts b/src/server/session/agents/monitor.ts
index 673be99be..e1709f5e6 100644
--- a/src/server/session/agents/monitor.ts
+++ b/src/server/session/agents/monitor.ts
@@ -1,6 +1,6 @@
import { ExitHandler } from "./applied_session_agent";
import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config";
-import Repl, { ReplAction } from "../../repl";
+import Repl, { ReplAction } from "../utilities/repl";
import { isWorker, setupMaster, on, Worker, fork } from "cluster";
import { IPC } from "../utilities/ipc";
import { red, cyan, white, yellow, blue, green } from "colors";
@@ -9,39 +9,24 @@ import { Utils } from "../../../Utils";
import { validate, ValidationError } from "jsonschema";
import { Utilities } from "../utilities/utilities";
import { readFileSync } from "fs";
-
-export namespace Monitor {
-
- export interface NotifierHooks {
- key?: (key: string) => (boolean | Promise<boolean>);
- crash?: (error: Error) => (boolean | Promise<boolean>);
- }
-
- export interface Action {
- message: string;
- args: any;
- }
-
- export type ServerMessageHandler = (action: Action) => void | Promise<void>;
-
-}
+import { EventEmitter } from "events";
/**
* 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 class Monitor {
+export class Monitor extends EventEmitter {
private static count = 0;
+ private finalized = false;
private exitHandlers: ExitHandler[] = [];
- private readonly notifiers: Monitor.NotifierHooks | undefined;
private readonly config: Configuration;
private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {};
private activeWorker: Worker | undefined;
private key: string | undefined;
private repl: Repl;
- public static Create(notifiers?: Monitor.NotifierHooks) {
+ public static Create() {
if (isWorker) {
IPC.dispatchMessage(process, {
action: {
@@ -58,7 +43,7 @@ export class Monitor {
console.error(red("cannot create more than one monitor."));
process.exit(1);
} else {
- return new Monitor(notifiers);
+ return new Monitor();
}
}
@@ -141,14 +126,12 @@ export class Monitor {
*/
public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined;
- private constructor(notifiers?: Monitor.NotifierHooks) {
- this.notifiers = notifiers;
+ private constructor() {
+ super();
console.log(this.timestamp(), cyan("initializing session..."));
-
this.config = this.loadAndValidateConfiguration();
- this.initializeSessionKey();
// determines whether or not we see the compilation / initialization / runtime output of each child server process
const output = this.config.showServerOutput ? "inherit" : "ignore";
setupMaster({ stdio: ["ignore", output, output, "ipc"] });
@@ -174,6 +157,14 @@ export class Monitor {
});
this.repl = this.initializeRepl();
+ }
+
+ public finalize = (): void => {
+ if (this.finalized) {
+ throw new Error("Session monitor is already finalized");
+ }
+ this.finalized = true;
+ this.emit(Monitor.IntrinsicEvents.KeyGenerated, this.key = Utils.GenerateGuid());
this.spawn();
}
@@ -198,22 +189,6 @@ export class Monitor {
}
/**
- * If the caller has indicated an interest
- * in being notified of this feature, creates
- * a GUID for this session that can, for example,
- * be used as authentication for killing the server
- * (checked externally).
- */
- private initializeSessionKey = async (): Promise<void> => {
- if (this.notifiers?.key) {
- this.key = Utils.GenerateGuid();
- const success = await this.notifiers.key(this.key);
- const statement = success ? green("distributed session key to recipients") : red("distribution of session key failed");
- this.mainLog(statement);
- }
- }
-
- /**
* Reads in configuration .json file only once, in the master thread
* and pass down any variables the pertinent to the child processes as environment variables.
*/
@@ -351,12 +326,10 @@ export class Monitor {
this.killSession(reason, graceful, errorCode);
break;
case "notify_crash":
- if (this.notifiers?.crash) {
- const { error } = args;
- const success = await this.notifiers.crash(error);
- const statement = success ? green("distributed crash notification to recipients") : red("distribution of crash notification failed");
- this.mainLog(statement);
- }
+ this.emit(Monitor.IntrinsicEvents.CrashDetected, args.error);
+ break;
+ case Monitor.IntrinsicEvents.ServerRunning:
+ this.emit(Monitor.IntrinsicEvents.ServerRunning, args.firstTime);
break;
case "set_port":
const { port, value, immediateRestart } = args;
@@ -374,4 +347,21 @@ export class Monitor {
});
}
+}
+
+export namespace Monitor {
+
+ export interface Action {
+ message: string;
+ args: any;
+ }
+
+ export type ServerMessageHandler = (action: Action) => void | Promise<void>;
+
+ export enum IntrinsicEvents {
+ KeyGenerated = "key_generated",
+ CrashDetected = "crash_detected",
+ ServerRunning = "server_running"
+ }
+
} \ No newline at end of file
diff --git a/src/server/session/agents/server_worker.ts b/src/server/session/agents/server_worker.ts
index 6ed385151..e9fdaf923 100644
--- a/src/server/session/agents/server_worker.ts
+++ b/src/server/session/agents/server_worker.ts
@@ -3,6 +3,7 @@ import { isMaster } from "cluster";
import { IPC } from "../utilities/ipc";
import { red, green, white, yellow } from "colors";
import { get } from "request-promise";
+import { Monitor } from "./monitor";
/**
* Effectively, each worker repairs the connection to the server by reintroducing a consistent state
@@ -19,6 +20,7 @@ export class ServerWorker {
private pollingFailureTolerance: number;
private pollTarget: string;
private serverPort: number;
+ private isInitialized = false;
public static Create(work: Function) {
if (isMaster) {
@@ -136,6 +138,8 @@ export class ServerWorker {
if (!this.shouldServerBeResponsive) {
// notify monitor thread that the server is up and running
this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
+ this.sendMonitorAction(Monitor.IntrinsicEvents.ServerRunning, { firstTime: !this.isInitialized });
+ this.isInitialized = true;
}
this.shouldServerBeResponsive = true;
} catch (error) {
diff --git a/src/server/repl.ts b/src/server/session/utilities/repl.ts
index ad55b6aaa..643141286 100644
--- a/src/server/repl.ts
+++ b/src/server/session/utilities/repl.ts
@@ -54,7 +54,7 @@ export default class Repl {
}
}
- private success = (command: string) => `${this.resolvedIdentifier()} completed execution of ${white(command)}`;
+ private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`;
public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
const existing = this.commandMap.get(basename);