diff options
-rw-r--r-- | src/server/DashSession/DashSessionAgent.ts | 213 | ||||
-rw-r--r-- | src/server/DashSession/crash_instructions.txt | 14 | ||||
-rw-r--r-- | src/server/DashSession/remote_debug_instructions.txt (renamed from src/server/remote_debug_instructions.txt) | 0 | ||||
-rw-r--r-- | src/server/DashSessionAgent.ts | 168 | ||||
-rw-r--r-- | src/server/index.ts | 2 | ||||
-rw-r--r-- | src/server/session/agents/applied_session_agent.ts | 9 | ||||
-rw-r--r-- | src/server/session/agents/monitor.ts | 84 | ||||
-rw-r--r-- | src/server/session/agents/server_worker.ts | 4 | ||||
-rw-r--r-- | src/server/session/utilities/repl.ts (renamed from src/server/repl.ts) | 2 |
9 files changed, 275 insertions, 221 deletions
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts new file mode 100644 index 000000000..b031c177e --- /dev/null +++ b/src/server/DashSession/DashSessionAgent.ts @@ -0,0 +1,213 @@ +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 = ["brownptcdash@gmail.com"]; + 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, "./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, "./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; + mainLog(green("dispatching session key...")); + const failures = await Email.dispatchAll({ + to: this.notificationRecipients, + subject: "Dash Release Session Admin Authentication Key", + content: `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}` + }); + if (failures) { + failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(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 failures = await Email.dispatchAll({ + to: this.notificationRecipients, + subject: "Dash Web Server Crash", + content: this.generateCrashInstructions(crashCause) + }); + if (failures) { + failures.map(({ recipient, error: { message } }) => this.sessionMonitor.mainLog(red(`dispatch failure @ ${recipient} (${yellow(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)); + } + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/crash_instructions.txt b/src/server/DashSession/crash_instructions.txt new file mode 100644 index 000000000..65417919d --- /dev/null +++ b/src/server/DashSession/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/remote_debug_instructions.txt index c279c460a..c279c460a 100644 --- a/src/server/remote_debug_instructions.txt +++ b/src/server/DashSession/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); |