aboutsummaryrefslogtreecommitdiff
path: root/src/server/session
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/session')
-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.ts128
4 files changed, 174 insertions, 51 deletions
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/session/utilities/repl.ts b/src/server/session/utilities/repl.ts
new file mode 100644
index 000000000..643141286
--- /dev/null
+++ b/src/server/session/utilities/repl.ts
@@ -0,0 +1,128 @@
+import { createInterface, Interface } from "readline";
+import { red, green, white } from "colors";
+
+export interface Configuration {
+ identifier: () => string | string;
+ onInvalid?: (command: string, validCommand: boolean) => string | string;
+ onValid?: (success?: string) => string | string;
+ isCaseSensitive?: boolean;
+}
+
+export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
+export interface Registration {
+ argPatterns: RegExp[];
+ action: ReplAction;
+}
+
+export default class Repl {
+ private identifier: () => string | string;
+ private onInvalid: ((command: string, validCommand: boolean) => string) | string;
+ private onValid: ((success: string) => string) | string;
+ private isCaseSensitive: boolean;
+ private commandMap = new Map<string, Registration[]>();
+ public interface: Interface;
+ private busy = false;
+ private keys: string | undefined;
+
+ constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
+ this.identifier = prompt;
+ this.onInvalid = onInvalid || this.usage;
+ this.onValid = onValid || this.success;
+ this.isCaseSensitive = isCaseSensitive ?? true;
+ this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
+ }
+
+ private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
+
+ private usage = (command: string, validCommand: boolean) => {
+ if (validCommand) {
+ const formatted = white(command);
+ const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n'));
+ return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
+ } else {
+ const resolved = this.keys;
+ if (resolved) {
+ return resolved;
+ }
+ const members: string[] = [];
+ const keys = this.commandMap.keys();
+ let next: IteratorResult<string>;
+ while (!(next = keys.next()).done) {
+ members.push(next.value);
+ }
+ return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
+ }
+ }
+
+ 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);
+ const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
+ const registration = { argPatterns: converted, action };
+ if (existing) {
+ existing.push(registration);
+ } else {
+ this.commandMap.set(basename, [registration]);
+ }
+ }
+
+ private invalid = (command: string, validCommand: boolean) => {
+ console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand)));
+ this.busy = false;
+ }
+
+ private valid = (command: string) => {
+ console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
+ this.busy = false;
+ }
+
+ private considerInput = async (line: string) => {
+ if (this.busy) {
+ console.log(red("Busy"));
+ return;
+ }
+ this.busy = true;
+ line = line.trim();
+ if (this.isCaseSensitive) {
+ line = line.toLowerCase();
+ }
+ const [command, ...args] = line.split(/\s+/g);
+ if (!command) {
+ return this.invalid(command, false);
+ }
+ const registered = this.commandMap.get(command);
+ if (registered) {
+ const { length } = args;
+ const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
+ for (const { argPatterns, action } of candidates) {
+ const parsed: string[] = [];
+ let matched = true;
+ if (length) {
+ for (let i = 0; i < length; i++) {
+ let matches: RegExpExecArray | null;
+ if ((matches = argPatterns[i].exec(args[i])) === null) {
+ matched = false;
+ break;
+ }
+ parsed.push(matches[0]);
+ }
+ }
+ if (!length || matched) {
+ const result = action(parsed);
+ const resolve = () => this.valid(`${command} ${parsed.join(" ")}`);
+ if (result instanceof Promise) {
+ result.then(resolve);
+ } else {
+ resolve();
+ }
+ return;
+ }
+ }
+ this.invalid(command, true);
+ } else {
+ this.invalid(command, false);
+ }
+ }
+
+} \ No newline at end of file