diff options
-rw-r--r-- | src/client/DocServer.ts | 4 | ||||
-rw-r--r-- | src/server/DashSession.ts | 89 | ||||
-rw-r--r-- | src/server/Session/session.ts | 69 | ||||
-rw-r--r-- | src/server/index.ts | 2 |
4 files changed, 92 insertions, 72 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 47c63bfb7..ed7fbd7ba 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -82,7 +82,9 @@ export namespace DocServer { Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); - Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, () => alert("Your connection to the server has been terminated.")); + Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, () => { + alert("Your connection to the server has been terminated."); + }); } function errorFunc(): never { diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts index 9c36fa17f..c0ebc9687 100644 --- a/src/server/DashSession.ts +++ b/src/server/DashSession.ts @@ -3,59 +3,58 @@ import { Email } from "./ActionUtilities"; import { red, yellow } from "colors"; import { SolrManager } from "./ApiManagers/SearchManager"; import { execSync } from "child_process"; -import { isMaster } from "cluster"; import { Utils } from "../Utils"; import { WebSocket } from "./Websocket/Websocket"; import { MessageStore } from "./Message"; import { launchServer } from "."; -const notificationRecipients = ["samuel_wilkins@brown.edu"]; -const signature = "-Dash Server Session Manager"; +/** +* 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 Session.AppliedSessionAgent { -const monitorHooks: Session.MonitorNotifierHooks = { - key: async (key, masterLog) => { - const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${signature}`; - const failures = await Email.dispatchAll(notificationRecipients, "Server Termination Key", content); - if (failures) { - failures.map(({ recipient, error: { message } }) => masterLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`))); - return false; - } - return true; - }, - crash: async ({ name, message, stack }, masterLog) => { - 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${signature}`; - const failures = await Email.dispatchAll(notificationRecipients, "Dash Web Server Crash", content); - if (failures) { - failures.map(({ recipient, error: { message } }) => masterLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`))); - return false; - } - return true; - } -}; + private readonly notificationRecipients = ["samuel_wilkins@brown.edu"]; + private readonly signature = "-Dash Server Session Manager"; -export class DashSessionAgent extends Session.AppliedSessionAgent { + protected async launchMonitor() { + const monitor = await Session.initializeMonitorThread({ + key: async (key, masterLog) => { + const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`; + const failures = await Email.dispatchAll(this.notificationRecipients, "Server Termination Key", content); + if (failures) { + failures.map(({ recipient, error: { message } }) => masterLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`))); + return false; + } + return true; + }, + crash: async ({ name, message, stack }, masterLog) => { + 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(this.notificationRecipients, "Dash Web Server Crash", content); + if (failures) { + failures.map(({ recipient, error: { message } }) => masterLog(red(`dispatch failure @ ${recipient} (${yellow(message)})`))); + return false; + } + return true; + } + }); + monitor.addReplCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] })); + monitor.addReplCommand("solr", [/start|stop/g], args => SolrManager.SetRunning(args[0] === "start")); + return monitor; + } - /** - * 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. - */ - protected async launchImplementation() { - if (isMaster) { - this.sessionMonitor = await Session.initializeMonitorThread(monitorHooks); - this.sessionMonitor.addReplCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] })); - this.sessionMonitor.addReplCommand("solr", [/start|stop/g], args => SolrManager.SetRunning(args[0] === "start")); - } else { - this.serverWorker = await Session.initializeWorkerThread(launchServer); // server initialization delegated to worker - this.serverWorker.addExitHandler(() => Utils.Emit(WebSocket._socket, MessageStore.ConnectionTerminated, "Manual")); - } + protected async launchServerWorker() { + const worker = await Session.initializeWorkerThread(launchServer); // server initialization delegated to worker + worker.addExitHandler(() => Utils.Emit(WebSocket._socket, MessageStore.ConnectionTerminated, "Manual")); + return worker; } }
\ No newline at end of file diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts index b22b6404d..cc9e7dd1a 100644 --- a/src/server/Session/session.ts +++ b/src/server/Session/session.ts @@ -22,46 +22,44 @@ export namespace Session { export abstract class AppliedSessionAgent { + public killSession(graceful = true) { + const target = isMaster ? this.sessionMonitor : this.serverWorker; + target.killSession(graceful); + } + private launched = false; - protected sessionMonitorRef: Session.Monitor | undefined; + private sessionMonitorRef: Session.Monitor | undefined; public get sessionMonitor(): Session.Monitor { if (!isMaster) { throw new Error("Cannot access the session monitor directly from the server worker thread"); } return this.sessionMonitorRef!; } - public set sessionMonitor(monitor: Session.Monitor) { - if (!isMaster) { - throw new Error("Cannot set the session monitor directly from the server worker thread"); - } - this.sessionMonitorRef = monitor; - } - protected serverWorkerRef: Session.ServerWorker | undefined; + private serverWorkerRef: Session.ServerWorker | undefined; public get serverWorker(): Session.ServerWorker { if (isMaster) { throw new Error("Cannot access the server worker directly from the session monitor thread"); } return this.serverWorkerRef!; } - public set serverWorker(worker: Session.ServerWorker) { - if (isMaster) { - throw new Error("Cannot set the server worker directly from the session monitor thread"); - } - this.serverWorkerRef = worker; - } public async launch(): Promise<void> { if (!this.launched) { this.launched = true; - await this.launchImplementation(); + if (isMaster) { + this.sessionMonitorRef = await this.launchMonitor(); + } else { + this.serverWorkerRef = await this.launchServerWorker(); + } } else { throw new Error("Cannot launch a session thread more than once per process."); } } - protected abstract async launchImplementation(): Promise<void>; + protected abstract async launchMonitor(): Promise<Session.Monitor>; + protected abstract async launchServerWorker(): Promise<Session.ServerWorker>; } @@ -92,11 +90,14 @@ export namespace Session { setPort: (port: "server" | "socket" | string, value: number, immediateRestart: boolean) => void; killSession: (graceful?: boolean) => never; addReplCommand: (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => void; - addChildMessageHandler: (message: string, handler: ActionHandler) => void; + addServerMessageListener: (message: string, handler: ActionHandler) => void; + removeServerMessageListener: (message: string, handler: ActionHandler) => void; + clearServerMessageListeners: (message: string) => void; } export interface ServerWorker { - killSession: () => void; + killSession: (graceful?: boolean) => void; + sendSessionAction: (message: string, args?: any) => void; addExitHandler: (handler: ExitHandler) => void; } @@ -176,7 +177,7 @@ export namespace Session { export async function initializeMonitorThread(notifiers?: MonitorNotifierHooks): Promise<Monitor> { console.log(timestamp(), cyan("initializing session...")); let activeWorker: Worker; - const childMessageHandlers: { [message: string]: ActionHandler } = {}; + const onMessage: { [message: string]: ActionHandler[] | undefined } = {}; // read in configuration .json file only once, in the master thread // pass down any variables the pertinent to the child processes as environment variables @@ -275,7 +276,7 @@ export namespace Session { switch (message) { case "kill": log(red("an authorized user has manually ended the server session")); - killSession(); + killSession(args.graceful); case "notify_crash": if (notifiers && notifiers.crash) { const { error } = args; @@ -287,9 +288,9 @@ export namespace Session { const { port, value, immediateRestart } = args; setPort(port, value, immediateRestart); default: - const handler = childMessageHandlers[message]; - if (handler) { - handler({ message, args }); + const handlers = onMessage[message]; + if (handlers) { + handlers.forEach(handler => handler({ message, args })); } } } else if (lifecycle) { @@ -333,7 +334,24 @@ export namespace Session { // returned to allow the caller to add custom commands return { addReplCommand: repl.registerCommand, - addChildMessageHandler: (message: string, handler: ActionHandler) => { childMessageHandlers[message] = handler; }, + addServerMessageListener: (message: string, handler: ActionHandler) => { + const handlers = onMessage[message]; + if (handlers) { + handlers.push(handler); + } else { + onMessage[message] = [handler]; + } + }, + removeServerMessageListener: (message: string, handler: ActionHandler) => { + const handlers = onMessage[message]; + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + }, + clearServerMessageListeners: (message: string) => onMessage[message] = undefined, restartServer, killSession, setPort, @@ -431,7 +449,8 @@ export namespace Session { return { addExitHandler: (handler: ExitHandler) => exitHandlers.push(handler), - killSession: () => process.send!({ action: { message: "kill" } }) + killSession: (graceful = true) => process.send!({ action: { message: "kill", args: { graceful } } }), + sendSessionAction: (message: string, args?: any) => process.send!({ action: { message, args } }) }; } diff --git a/src/server/index.ts b/src/server/index.ts index f18e9abb6..ffab0f380 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -92,7 +92,7 @@ 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%;'/>"); - sessionAgent.serverWorker.killSession(); + sessionAgent.killSession(); } else { res.redirect("/home"); } |