From 125ef30e833127a89a2b1ff08b6b61291d5c8e91 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Thu, 15 Aug 2024 17:32:50 -0400
Subject: [PATCH 01/47] timestamps: add rudimentary timestamping to console
output, as nc-multiplex doesn't have a log file
---
modules/nc-logging-utils.js | 110 ++++++++++++++++++++++++++++++++++++
nc-launch-instance.js | 15 +++--
nc-multiplex.js | 54 ++++++++++--------
3 files changed, 149 insertions(+), 30 deletions(-)
create mode 100644 modules/nc-logging-utils.js
diff --git a/modules/nc-logging-utils.js b/modules/nc-logging-utils.js
new file mode 100644
index 0000000..43a9c2a
--- /dev/null
+++ b/modules/nc-logging-utils.js
@@ -0,0 +1,110 @@
+/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\
+
+ FILE NAMING UTILITIES
+
+\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/
+
+const WEEKDAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
+
+/// UTILITY METHODS ///////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return an object with values from nanosecond-resolution timer as
+ * both {time_sec,time_nsec} and {nsec_now}, Use nsec_now if you just want
+ * a number that is a single number. The nanoseconds value can be a
+ * fractional value.
+ */
+function m_GetNanoTimeProps() {
+ if (process === undefined)
+ return {
+ error: 'nanotime available on server-side only',
+ time_sec: 'error',
+ time_nsec: 'error',
+ nsec_now: 'error'
+ };
+ let hrt = process.hrtime();
+ let nsecs = Math.floor(hrt[0] * 1e9 + hrt[1]);
+ return {
+ time_sec: hrt[0],
+ time_nsec: hrt[1],
+ nsec_now: nsecs
+ };
+}
+
+/// TIME/DATE STRINGS /////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return date string 'HH:MM:SS' for */
+function strTimeStamp() {
+ let date = new Date();
+ let hh = `0${date.getHours()}`.slice(-2);
+ let mm = `0${date.getMinutes()}`.slice(-2);
+ let ss = `0${date.getSeconds()}`.slice(-2);
+ return `${hh}:${mm}:${ss}`;
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return date string 'HH:MM:SS:MS' for */
+function strTimeStampMS() {
+ let date = new Date();
+ let hh = `0${date.getHours()}`.slice(-2);
+ let mm = `0${date.getMinutes()}`.slice(-2);
+ let ss = `0${date.getSeconds()}`.slice(-2);
+ let ms = `0${date.getMilliseconds()}`;
+ return `${hh}:${mm}:${ss}:${ms}`;
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return date string YYYY/MM/DD WEEKDAY for use as a prefix in log file
+ * content
+ */
+function strDateStamp() {
+ let date = new Date();
+ let mm = `0${date.getMonth() + 1}`.slice(-2);
+ let dd = `0${date.getDate()}`.slice(-2);
+ let day = WEEKDAYS[date.getDay()];
+ let yyyy = date.getFullYear();
+ return `${yyyy}/${mm}/${dd} ${day}`;
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return date-time-args filename YYYY-MMDD-args-HHMMSS, where args is
+ * the string parameters passed to this function separated by hyphens.
+ * This creates filenames that group by DATE, filetype, and then time
+ * of creation.
+ */
+function strTimeDatedFilename(...args) {
+ // construct filename
+ let date = new Date();
+ let dd = `0${date.getDate()}`.slice(-2);
+ let mm = `0${date.getMonth() + 1}`.slice(-2);
+ let hms = `0${date.getHours()}`.slice(-2);
+ hms += `0${date.getMinutes()}`.slice(-2);
+ hms += `0${date.getSeconds()}`.slice(-2);
+ let filename;
+ filename = date.getFullYear().toString();
+ filename += `-${mm}${dd}`;
+ let c = arguments.length;
+ if (c) filename = filename.concat('-', ...args);
+ filename += `-${hms}`;
+ return filename;
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return a string of integer nanoseconds from the high resolution timer */
+function strNanoTimeStamp() {
+ // get high resolution time
+ const { now_ns } = m_GetNanoTimeProps();
+ const val = Math.floor(now_ns);
+ return val.toString();
+}
+
+/// MODULE EXPORTS ////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+module.exports = {
+ // string time/date stamps
+ strTimeStamp,
+ strTimeStampMS,
+ strDateStamp,
+ strNanoTimeStamp,
+ strTimeDatedFilename,
+ // compatibility
+ TimeStamp: strTimeStamp,
+ TimeStampMS: strTimeStampMS,
+ DateStamp: strDateStamp,
+ DatedFilename: strTimeDatedFilename
+};
diff --git a/nc-launch-instance.js b/nc-launch-instance.js
index 179f297..5a5c5c8 100755
--- a/nc-launch-instance.js
+++ b/nc-launch-instance.js
@@ -19,6 +19,11 @@ const shell = require('shelljs');
const NCUTILS = require('./modules/nc-utils');
const { NC_PATH, NC_SERVER_PATH, NC_CONFIG_PATH } = require('./nc-launch-config');
+// SRI HACK IN TIMESTAMP
+const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
+const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
+const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
+
const PRE = '...nc-launch-instance:';
function writeConfig(data) {
@@ -35,14 +40,14 @@ function promiseServer(port) {
}
process.on('message', data => {
- console.log(PRE);
- console.log(PRE, 'STARTING DB', data.db);
- console.log(PRE);
+ console.log(PRE,$T());
+ console.log(PRE,$T(), 'STARTING DB', data.db);
+ console.log(PRE,$T());
- console.log(PRE, '1. Setting netcreate-config.js.');
+ console.log(PRE,$T(), '1. Setting netcreate-config.js.');
writeConfig(data);
- console.log(PRE, '2. Starting server');
+ console.log(PRE,$T(), '2. Starting server');
startServer(data.port);
});
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 5d1101a..cf154a6 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -102,13 +102,18 @@ const path = require('path');
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
+
+// SRI HACK IN TIMESTAMP
+const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
+const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
+const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
+
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
const NCUTILS = require('./modules/nc-utils.js');
const { NC_SERVER_PATH, NC_URL_CONFIG } = require('./nc-launch-config');
-
const PRE = '...nc-multiplex: '; // console.log prefix
// SETTINGS
@@ -161,12 +166,12 @@ exec('node --version', (error, stdout, stderr) => {
stdout = stdout.trim();
if (stdout !== NODE_VER) {
console.log('\x1b[97;41m');
- console.log(PRE, '*** NODE VERSION MISMATCH ***');
- console.log(PRE, '.. expected', NODE_VER, 'got', stdout);
- console.log(PRE, '.. did you remember to run nvm use?\x1b[0m');
+ console.log(PRE, $T(), '*** NODE VERSION MISMATCH ***');
+ console.log(PRE, $T(), '.. expected', NODE_VER, 'got', stdout);
+ console.log(PRE, $T(), '.. did you remember to run nvm use?\x1b[0m');
console.log('');
}
- console.log(PRE, 'NODE VERSION:', stdout, 'OK');
+ console.log(PRE, $T(), 'NODE VERSION:', stdout, 'OK');
}
});
@@ -570,9 +575,9 @@ function PromiseApp(db) {
// send a message back to this handler, which in turn
// sends the new spec back to SpawnApp
forked.on('message', msg => {
- console.log(PRE + 'Received message from spawned fork:', msg);
+ console.log(PRE, $T(), 'Received message from spawned fork:', msg);
console.log(PRE);
- console.log(PRE + `${db} STARTED!`);
+ console.log(PRE, $T(), `${db} STARTED!`);
console.log(PRE);
const newProcessDef = {
db,
@@ -627,9 +632,8 @@ function OutOfMemory() {
// ----------------------------------------------------------------------------
// INIT
-console.log(`\n\n\n`);
-console.log(PRE);
-console.log(PRE + 'STARTED!');
+console.log(`\n\n\n`)
+console.log(PRE, $T(), 'STARTED!');
console.log(PRE);
// START BASE APP
@@ -668,10 +672,10 @@ async function RouterGraph(req) {
let route = childProcesses.find(route => route.db === db);
if (route) {
// a) Yes. Use existing route!
- console.log(PRE + '--> mapping to ', route.db, route.port);
+ console.log(PRE + $T() + '--> mapping to ', route.db, route.port);
port = route.port;
} else if (PortPoolIsEmpty()) {
- console.log(PRE + '--> No more ports. Not spawning', db);
+ console.log(PRE + $T() + '--> No more ports. Not spawning', db);
// b) No more ports available.
path = `/error_out_of_ports`;
} else if (OutOfMemory()) {
@@ -679,7 +683,7 @@ async function RouterGraph(req) {
path = `/error_out_of_memory`;
} else if (ALLOW_NEW || ALLOW_SPAWN) {
// c) Not defined yet, Create a new one.
- console.log(PRE + '--> not running yet, starting new', db);
+ console.log(PRE + $T() + '--> not running yet, starting new', db);
port = await SpawnApp(db);
} else {
// c) Not defined yet. Report error.
@@ -759,31 +763,31 @@ function SendErrorResponse(res, msg) {
// HANDLE NO DATABASE -- RETURN ERROR
app.get('/error_no_database', (req, res) => {
- console.log(PRE + '================== Handling ERROR NO DATABASE!');
+ console.log(PRE + $T() + '================== Handling ERROR NO DATABASE!');
SendErrorResponse(res, 'This graph is not currently open.');
});
// HANDLE NOT AUTHORIZED -- RETURN ERROR
app.get('/error_not_authorized', (req, res) => {
- console.log(PRE + '================== Handling ERROR NOT AUTHORIZED!');
+ console.log(PRE + $T() + '================== Handling ERROR NOT AUTHORIZED!');
SendErrorResponse(res, 'Not Authorized.');
});
// HANDLE OUT OF PORTS -- RETURN ERROR
app.get('/error_out_of_ports', (req, res) => {
- console.log(PRE + '================== Handling ERROR OUT OF PORTS!');
+ console.log(PRE + $T() + '================== Handling ERROR OUT OF PORTS!');
SendErrorResponse(res, "Ran out of ports. Can't start the graph.");
});
// HANDLE OUT OF MEMORY -- RETURN ERROR
app.get('/error_out_of_memory', (req, res) => {
- console.log(PRE + '================== Handling ERROR OUT OF MEMORY!');
+ console.log(PRE + $T() + '================== Handling ERROR OUT OF MEMORY!');
SendErrorResponse(res, "Ran out of Memory. Can't start the graph.");
});
// HANDLE MISSING TRAILING ".../" -- RETURN ERROR
app.get('/graph/:file', (req, res) => {
- console.log(PRE + '================== Handling BAD URL!');
+ console.log(PRE + $T() + '================== Handling BAD URL!');
SendErrorResponse(res, "Bad URL. Missing trailing '/'.");
});
@@ -792,7 +796,7 @@ app.get('/graph/:file', (req, res) => {
// HANDLE "/kill/:graph" -- KILL REQUEST
app.get('/kill/:graph/', (req, res) => {
- console.log(PRE + '================== Handling / KILL!');
+ console.log(PRE + $T() + '================== Handling / KILL!');
const db = req.params.graph;
res.set('Content-Type', 'text/html');
let response = `
NetCreate Manager
`;
@@ -819,7 +823,7 @@ app.get('/kill/:graph/', (req, res) => {
// HANDLE "/maketoken" -- GENERATE TOKENS
app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
- console.log(PRE + '================== Handling / MAKE TOKEN!');
+ console.log(PRE + $T() + '================== Handling / MAKE TOKEN!');
const { clsid, projid, dataset, numgroups } = req.params;
let response = MakeToken(clsid, projid, dataset, parseInt(numgroups));
res.set('Content-Type', 'text/html');
@@ -849,7 +853,7 @@ app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
// HANDLE "/manage" -- MANAGER PAGE
app.get('/manage', (req, res) => {
- console.log(PRE + '================== Handling / MANAGE!');
+ console.log(PRE + $T() + '================== Handling / MANAGE!');
if (CookieIsValid(req)) {
res.set('Content-Type', 'text/html');
res.send(RenderManager());
@@ -859,7 +863,7 @@ app.get('/manage', (req, res) => {
});
app.get('/login', (req, res) => {
- console.log(PRE + '================== Handling / LOGIN!');
+ console.log(PRE + $T() + '================== Handling / LOGIN!');
if (CookieIsValid(req)) {
// Cookie already set, no need to log in, redirect to manage
res.redirect(`/manage`);
@@ -871,7 +875,7 @@ app.get('/login', (req, res) => {
});
app.post('/authorize', (req, res) => {
- console.log(PRE + '================== Handling / AUTHORIZE!');
+ console.log(PRE + $T() + '================== Handling / AUTHORIZE!');
let str = new String(req.body.password);
if (req.body.password === PASSWORD) {
res.cookie('nc-multiplex-auth', PASSWORD_HASH, {
@@ -888,7 +892,7 @@ app.post('/authorize', (req, res) => {
// HANDLE "/" -- HOME PAGE
app.get('/', (req, res) => {
- console.log(PRE + '================== Handling / ROOT!');
+ console.log(PRE + $T() + '================== Handling / ROOT!');
if (HOMEPAGE_EXISTS) {
res.sendFile(path.join(__dirname, 'home.html'));
} else {
@@ -938,4 +942,4 @@ app.use(
//
// START PROXY
-app.listen(PORT_ROUTER, () => console.log(PRE + `running on port ${PORT_ROUTER}.`));
+app.listen(PORT_ROUTER, () => console.log(PRE, $T(), `running on port ${PORT_ROUTER}.`));
From ca055d9cba2ed7d6efea2e160f5ea9cef8887c44 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Mon, 19 Aug 2024 15:06:34 -0400
Subject: [PATCH 02/47] dev-sri/timestamps: fix broken eslintrc
---
.eslintrc.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 9fd236d..42a06f6 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -26,7 +26,7 @@ const config = {
something else (e.g. parser) that ESLINT can make use of.
See: eslint.org/docs/user-guide/configuring#use-a-plugin
:*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
- plugins: ['react'],
+ plugins: [],
extends: [
'eslint:recommended', // standard recommendations
// 'plugin:react/recommended', // handle jsx syntax
From 2ce78b122025acf00e6bd2942bde30d6063c8f81 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Mon, 19 Aug 2024 15:07:01 -0400
Subject: [PATCH 03/47] sri-timestamp: duplicate of nc-multiplex to modify
---
nc-multiplex-sri.js | 945 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 945 insertions(+)
create mode 100644 nc-multiplex-sri.js
diff --git a/nc-multiplex-sri.js b/nc-multiplex-sri.js
new file mode 100644
index 0000000..cf154a6
--- /dev/null
+++ b/nc-multiplex-sri.js
@@ -0,0 +1,945 @@
+/*
+
+ nc-multiplex.js
+
+ This creates a node-based proxy server that will
+ spin up individual NetCreate graph instances
+ running on their own node processes.
+
+ To start this manually:
+ `node nc-multiplex.js`
+
+ Or use `npm run start`
+
+ Then go to `localhost` to view the manager.
+ (NOTE: This runs on port 80, so need to add a port)
+
+ The manager will list the running databases.
+
+ To start a new graph:
+ `http://localhost/graph/tacitus/`
+
+ If the graph already exists, it will be loaded.
+ Otherwise it will create a new graph.
+ (You need to be logged into the manager for this
+ to work.)
+
+ Refresh the manager to view running databases.
+
+
+ # Setting IP or Google Analytics code
+
+ Use the optional `--ip` or `--googlea` parameters if you need
+ to start the server with a specific IP address or google
+ analytics code. e.g.:
+
+ `node nc-multiplex.js --ip=192.168.1.40`
+ `node nc-multiplex.js --googlea=xxxxx`
+
+
+ # Route Scheme
+
+ / => localhost:80 Root: NetCreate Manager page
+ /graph//#/edit/uid => localhost:3x00/#/edit/uid
+ /*.[js,css,html] => localhost:3000/net-lib.js
+
+
+ # Port scheme
+
+ The proxy server runs on port 80.
+ Defined in `port_router`
+
+ Base application port is 3000
+ Base websocket port is 4000
+
+ When the app is started, we initialize a pool of ports
+ indices basedon the PROCESS_MAX value.
+
+ When a process is spawned, we grab from the pool of port
+ indices, then generate new port numbers based on the
+ index, where the app port and the websocket (net) port share
+ the same basic index, e.g.
+
+ {
+ index: 2,
+ appport: 3002,
+ netport: 4002
+ }
+
+ When the process is killed, the port index is returned
+ to the pool and re-used.
+
+
+ # netcreate-config.js / NC_CONFIG
+
+ NC_CONFIG is actually used by both the server-side scripts and
+ client-side scripts to set the active database, ip, ports,
+ netports, and google analytics code.
+
+ As such, it is generated twice:
+ 1. server-side: nc-start.js will generate a local file version into /build/app/assets
+ where it is used by brunch-server.js, brunch-config.js, and server-database.js
+ during the app start process.
+ 2. client-side: nc-multiplex.js will then dynamically generate netcreate-config.js
+ for each graph's http request.
+
+ REVIEW: There is a potential conflict server-side if two graphs
+ are started up at the same time and the newly generated netcreate-config.js
+ files cross each other.
+
+ REVIEW: The dynamically generated client-side version should probably be cached.
+
+*/
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// CONSTANTS
+
+const { createProxyMiddleware } = require('http-proxy-middleware');
+const { fork, exec } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+const cookieParser = require('cookie-parser');
+const crypto = require('crypto');
+
+// SRI HACK IN TIMESTAMP
+const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
+const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
+const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
+
+const app = express();
+app.use(express.urlencoded({ extended: true }));
+app.use(cookieParser());
+
+const NCUTILS = require('./modules/nc-utils.js');
+const { NC_SERVER_PATH, NC_URL_CONFIG } = require('./nc-launch-config');
+const PRE = '...nc-multiplex: '; // console.log prefix
+
+// SETTINGS
+const PORT_ROUTER = 80;
+const PORT_APP = 3000; // base port for nc apps
+const PORT_WS = 4000; // base port for websockets
+const DEFAULT_PASSWORD = 'kpop'; // override with SESAME file
+
+let HOMEPAGE_EXISTS; // Flag for existence of home.html override
+let PASSWORD; // Either default password or password in `SESAME` file
+let PASSWORD_HASH; // Hash generated from password
+let childProcesses = []; // array of forked process + meta info = { db, port, netport, portindex, process };
+
+// OPTIONS
+const PROCESS_MAX = 30; // Set this to limit the number of running processes
+// in order to keep a rein on CPU and MEM loads
+// If you set this higher than 100 you should make
+// sure you open inbound ports higher than 3100 and 4100
+
+const MEMORY_MIN = 256; // in MegaBytes
+// Don't start a new process if there is less than
+// MEMORY_MIN memory remaining.
+// In our testing with macOS and Ubuntu 18.04 on EC2:
+// * Each node process is generally ~30 MB.
+// * Servers stop responding with less than 100 MB remaining.
+
+const ALLOW_NEW = false; // default = false
+// false: App will respond with ERROR NO DATABASE if you enter
+// a url that points to a non-existent database.
+// true: Set to true to allow auto-spawning a new database via
+// url. e.g. going to `http://localhost/graph/newdb/`
+// would automatically create a new database if it
+// didn't already exist.
+
+const AUTH_MINUTES = 2; // default = 30
+// Number of minutes to authorize login cookie
+// After AUTH_MINUTES, the user wil have to re-login.
+
+// ----------------------------------------------------------------------------
+// check nvm version
+let NODE_VER;
+try {
+ NODE_VER = fs.readFileSync('./.nvmrc', 'utf8').trim();
+} catch (err) {
+ console.error('could not read .nvmrc', err);
+ throw Error(`Could not read .nvmrc ${err}`);
+}
+exec('node --version', (error, stdout, stderr) => {
+ if (stdout) {
+ stdout = stdout.trim();
+ if (stdout !== NODE_VER) {
+ console.log('\x1b[97;41m');
+ console.log(PRE, $T(), '*** NODE VERSION MISMATCH ***');
+ console.log(PRE, $T(), '.. expected', NODE_VER, 'got', stdout);
+ console.log(PRE, $T(), '.. did you remember to run nvm use?\x1b[0m');
+ console.log('');
+ }
+ console.log(PRE, $T(), 'NODE VERSION:', stdout, 'OK');
+ }
+});
+
+// ----------------------------------------------------------------------------
+// READ OPTIONAL ARGUMENTS
+//
+// To set ip address or google analytics code, call nc-multiplex with
+// arguments, e.g.
+//
+// `node nc-multiplex.js --ip=192.168.1.40`
+// `node nc-multiplex.js --googlea=xxxxx`
+//
+
+const argv = require('minimist')(process.argv.slice(2));
+const googlea = argv['googlea'];
+const ip = argv['ip'];
+
+// ----------------------------------------------------------------------------
+// SET HOME PAGE OVERRIDE
+//
+// If there's a 'home.html' file, serve that at '/'.
+//
+try {
+ fs.accessSync('home.html', fs.constants.R_OK);
+ HOMEPAGE_EXISTS = true;
+} catch (err) {
+ // no home page, use default
+ HOMEPAGE_EXISTS = false;
+}
+
+// ----------------------------------------------------------------------------
+// SET PASSWORD
+//
+// If there's a 'SESAME' file, use the password in there.
+// Otherwise, fallback to default.
+//
+try {
+ let sesame = fs.readFileSync('SESAME', 'utf8');
+ PASSWORD = sesame;
+} catch (err) {
+ // no password, use default
+ PASSWORD = DEFAULT_PASSWORD;
+}
+// Make Hash
+PASSWORD_HASH = GetHash(PASSWORD);
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// UTILITIES
+
+/**
+ * Number formatter
+ * From stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
+ * @param {integer} x
+ * @return {string} Number formatted with commas, e.g. 123,456
+ */
+function numberWithCommas(x) {
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+}
+
+/**
+ * Returns true if the db is currently running as a process
+ * @param {string} db
+ */
+function DBIsRunning(db) {
+ return childProcesses.find(route => route.db === db);
+}
+
+/**
+ * Generates a list of tokens using the NetCreate commoon-session module
+ * REVIEW: Requiring a module from the secondary netcreate-2018 repo
+ * is a little iffy.
+ * @param {string} clsId
+ * @param {string} projId
+ * @param {string} dataset
+ * @param {integer} numGroups
+ * @return {string}
+ */
+function MakeToken(clsId, projId, dataset, numGroups) {
+ const rpath = `${NC_SERVER_PATH}/app/unisys/common-session.js`;
+ const SESSION = require(rpath);
+ // from nc-logic.js
+ if (typeof clsId !== 'string')
+ return 'args: str classId, str projId, str dataset, int numGroups';
+ if (typeof projId !== 'string')
+ return 'args: str classId, str projId, str dataset, int numGroups';
+ if (typeof dataset !== 'string')
+ return 'args: str classId, str projId, str dataset, int numGroups';
+ if (clsId.length > 12) return 'classId arg1 should be 12 chars or less';
+ if (projId.length > 12) return 'classId arg1 should be 12 chars or less';
+ if (!Number.isInteger(numGroups)) return 'numGroups arg3 must be integer';
+ if (numGroups < 1) return 'numGroups arg3 must be positive integer';
+
+ let out = `TOKEN LIST for class '${clsId}' project '${projId}' dataset '${dataset}'\n\n`;
+ let pad = String(numGroups).length;
+ for (let i = 1; i <= numGroups; i++) {
+ let id = String(i);
+ id = id.padStart(pad, '0');
+ out += `group ${id}\t${SESSION.MakeToken(clsId, projId, i, dataset)}\n`;
+ }
+ return out;
+}
+
+/**
+ * Used to generate a hashed password for use in the cookie
+ * so that password text is not visible in the cookie.
+ * @param {string} pw
+ */
+function GetHash(pw) {
+ let hash = crypto.createHash('sha1').update(pw).digest('hex');
+ return hash;
+}
+
+/**
+ * HASH is generated from the PASSWORD
+ * @param {string} pw
+ */
+function CookieIsValid(req) {
+ if (!req || !req.cookies) return false;
+ // check against hash
+ let pw = req.cookies['nc-multiplex-auth'];
+ return pw === PASSWORD_HASH;
+}
+
+///// PORT POOL ---------------------------------------------------------------
+
+// Initialize port pool
+// port 0 is for the base app
+const port_pool = []; // array of available port indices, usu [1...100]
+for (let i = 0; i <= PROCESS_MAX; i++) {
+ port_pool.push(i);
+}
+/**
+ * Gets the next available port from the pool.
+ *
+ * @param {integer} index of route
+ * @return {object} JSON object definition, e.g.
+ * {
+ * index: integer // e.g. 3
+ * appport: integer // e.g. 3003
+ * netport: integer // e.g. 4003
+ * }
+ * or `undefined` if no items are left in the pool
+ */
+function PickPort() {
+ if (port_pool.length < 1) return undefined;
+ const index = port_pool.shift();
+ const result = {
+ index,
+ appport: PORT_APP + index,
+ netport: PORT_WS + index
+ };
+ return result;
+}
+/**
+ *
+ * @param {integer} index -- Port index to return to the pool
+ */
+function ReleasePort(index) {
+ if (port_pool.find(port => port === index))
+ throw 'ERROR: Port already in pool! This should not happen! ' + index;
+ port_pool.push(index);
+}
+/**
+ * Returns true if there are no more port indices left in the pool
+ * Used by /graph// route to check if it should spawn a new app
+ */
+function PortPoolIsEmpty() {
+ return port_pool.length < 1;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// RENDERERS
+
+const logoHtml =
+ '
`;
+ return response;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// PROCESS MANAGERS
+
+/**
+ * Use this to spawn a new node instance
+ * Calls PromiseApp.
+ *
+ * @param {string} db
+ * @return {integer} port to be used by router function
+ * in app.use(`/graph/:graph/:file`...).
+ */
+async function SpawnApp(db) {
+ try {
+ const newProcessDef = await PromiseApp(db);
+ AddChildProcess(newProcessDef);
+ return newProcessDef.port;
+ } catch (err) {
+ console.error(PRE + 'SpawnApp Failed with error', err);
+ }
+}
+
+/**
+ * Promises a new node NetCreate application process
+ *
+ * In general, don't call this directly. Use SpawnApp.
+ *
+ * This starts `nc-start.js` via a fork.
+ * `nc-start.js` will generate the `netcreate-config.js`
+ * configuration file, and then start the brunch server.
+ *
+ * When `nc-start.js` has completed, it sends a message back
+ * via fork messaging, at which point this promise is resolved
+ * and then we redirect the user to the new port.
+ *
+ * @param {string} db
+ * @resolve {object} sends the forked process and meta info
+ *
+ */
+function PromiseApp(db) {
+ return new Promise((resolve, reject) => {
+ const ports = PickPort();
+ if (ports === undefined) {
+ reject(`Unable to find a free port. ${db} not created.`);
+ }
+
+ // 1. Define the fork
+ const forked = fork('./nc-launch-instance.js');
+
+ // 2. Define fork success handler
+ // When the child node process is up and running, it will
+ // send a message back to this handler, which in turn
+ // sends the new spec back to SpawnApp
+ forked.on('message', msg => {
+ console.log(PRE, $T(), 'Received message from spawned fork:', msg);
+ console.log(PRE);
+ console.log(PRE, $T(), `${db} STARTED!`);
+ console.log(PRE);
+ const newProcessDef = {
+ db,
+ port: ports.appport,
+ netport: ports.netport,
+ portindex: ports.index,
+ googlea: googlea,
+ process: forked
+ };
+ resolve(newProcessDef); // pass to SpawnApp
+ });
+
+ // 3. Send message to start fork
+ // This sends the necessary startup prarameters to nc-start.js
+ // When nc-start is completed, it will call the message
+ // handler in #2 above
+ const ncStartParams = {
+ db,
+ port: ports.appport,
+ netport: ports.netport,
+ process: forked,
+ ip,
+ googlea
+ };
+ forked.send(ncStartParams);
+ });
+}
+
+/**
+ * Add the newProcess to the array of childProcesses
+ * but only if it doesn't already exist
+ * @param {object} route
+ */
+function AddChildProcess(newProcess) {
+ if (childProcesses.find(route => route.db === newProcess.db)) return;
+ childProcesses.push(newProcess);
+}
+
+/**
+ * Used to check if we have enough memory to start a new node process
+ * This is used to prevent node from starting too many processes.
+ */
+function OutOfMemory() {
+ let mem = process.memoryUsage();
+ return mem.heapTotal / 1024 - mem.heapUsed / 1024 < MEMORY_MIN;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// HTTP-PROXY-MIDDLEWARE ROUTING
+//
+
+// ----------------------------------------------------------------------------
+// INIT
+console.log(`\n\n\n`)
+console.log(PRE, $T(), 'STARTED!');
+console.log(PRE);
+
+// START BASE APP
+// This is needed to handle static file requests.
+// Most imports/requires do not specify the db route /graph/dbname/
+// so we need to provide a base app that responds to those static file
+// requests. This starts a generic "base" dataset at port 3000.
+SpawnApp('base');
+
+// ----------------------------------------------------------------------------
+// ROUTE FUNCTIONS
+
+/**
+ * RouterGraph
+ * @param {object} req
+ *
+ * The router function tries to route to the correct port by:
+ * a) if process is already running, use existing port
+ * b) if the process isn't running, spawn a new process
+ * and pass the port
+ * c) if no more ports are available, redirect back to the root.
+ *
+ */
+async function RouterGraph(req) {
+ const db = req.params.graph;
+ let port;
+ let path = '';
+
+ // Authenticate to allow spawning
+ let ALLOW_SPAWN = false;
+ if (CookieIsValid(req)) {
+ ALLOW_SPAWN = true;
+ }
+
+ // Is it already running?
+ let route = childProcesses.find(route => route.db === db);
+ if (route) {
+ // a) Yes. Use existing route!
+ console.log(PRE + $T() + '--> mapping to ', route.db, route.port);
+ port = route.port;
+ } else if (PortPoolIsEmpty()) {
+ console.log(PRE + $T() + '--> No more ports. Not spawning', db);
+ // b) No more ports available.
+ path = `/error_out_of_ports`;
+ } else if (OutOfMemory()) {
+ // c) Not enough memory to spawn new node instance
+ path = `/error_out_of_memory`;
+ } else if (ALLOW_NEW || ALLOW_SPAWN) {
+ // c) Not defined yet, Create a new one.
+ console.log(PRE + $T() + '--> not running yet, starting new', db);
+ port = await SpawnApp(db);
+ } else {
+ // c) Not defined yet. Report error.
+ path = `/error_no_database`;
+ }
+ return {
+ protocol: 'http:',
+ host: 'localhost',
+ port: port,
+ path: path
+ };
+}
+
+// ----------------------------------------------------------------------------
+// ROUTES
+
+// HANDLE `/graph/:graph/netcreate-config.js`
+//
+// The config file needs to be dynamically served for each node instance,
+// otherwise they would share (and clobber) the same static file.
+//
+// This has to go before `/graph/:graph/:file?` or it won't get triggered
+//
+app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
+ const db = req.params.graph;
+ let response = '';
+ console.log('############ returning netcreate-config.js for', db);
+ const child = childProcesses.find(child => child.db === db);
+ if (child) {
+ response += NCUTILS.GetNCConfig(child);
+ } else {
+ response += 'ERROR: No database found to netcreate-config.js: ' + db;
+ }
+ res.set('Content-Type', 'application/javascript');
+ res.send(response);
+});
+
+// HANDLE `/graph/:graph/:file?`
+//
+// * `:file` is optional. It catches db-specific file requests,
+// for example, the`netcreate-config.js` request.
+// * If there's a missing trailing "/", the URL is malformed
+//
+app.use(
+ '/graph/:graph/:file?',
+ createProxyMiddleware(
+ (pathname, req) => {
+ // only match if there is a trailing '/'
+ if (req.params.file) return true; // legit file
+ if (req.params.graph && req.originalUrl.endsWith('/')) return true; // legit graph
+ return false;
+ },
+ {
+ router: RouterGraph,
+ pathRewrite: function (path, req) {
+ // remove '/graph/db/' for the rerouted calls
+ // e.g. localhost/graph/hawaii/#/edit/mop => localhost:3000/#/edit/mop
+ return (rewrite = path.replace(`/graph/${req.params.graph}`, ''));
+ },
+ target: `http://localhost:3000`, // default fallback, router takes precedence
+ ws: true,
+ changeOrigin: true
+ }
+ )
+);
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - -
+// ERROR HANDLERS
+
+function SendErrorResponse(res, msg) {
+ res.set('Content-Type', 'text/html');
+ res.send(
+ `
`;
+
+ res.send(response);
+});
+
+// HANDLE "/maketoken" -- GENERATE TOKENS
+app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
+ console.log(PRE + $T() + '================== Handling / MAKE TOKEN!');
+ const { clsid, projid, dataset, numgroups } = req.params;
+ let response = MakeToken(clsid, projid, dataset, parseInt(numgroups));
+ res.set('Content-Type', 'text/html');
+ res.send(response);
+});
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - -
+// MANAGE
+//
+// Authentication
+//
+// Authentication uses a cookie with a hashed password.
+// The cookie expires after AUTH_MINUTES
+//
+// 1. /manage initially redirects to /login
+// 2. On the /login form, the administrator enters a password
+// 3. /login POSTS to /authorize
+// 4. /authorize checks the password against the PASSWORD
+// If there's no match, the user is redirected to /error_not_authorized
+// 5. /authorize then sets a cookie with the PASSWORD_HASH and
+// the user is redirected to /manage
+// 6. /manage checks the cookie against the PASSWORD_HASH
+// If the cookie matches, the manage page is displayed
+// If the cookie doesn't match, the user is redirected back to /login
+// 7. The cookie expires after AUTH_MINUTES
+//
+
+// HANDLE "/manage" -- MANAGER PAGE
+app.get('/manage', (req, res) => {
+ console.log(PRE + $T() + '================== Handling / MANAGE!');
+ if (CookieIsValid(req)) {
+ res.set('Content-Type', 'text/html');
+ res.send(RenderManager());
+ } else {
+ res.redirect(`/login`);
+ }
+});
+
+app.get('/login', (req, res) => {
+ console.log(PRE + $T() + '================== Handling / LOGIN!');
+ if (CookieIsValid(req)) {
+ // Cookie already set, no need to log in, redirect to manage
+ res.redirect(`/manage`);
+ } else {
+ // Show login form
+ res.set('Content-Type', 'text/html');
+ res.send(logoHtml + RenderLoginForm());
+ }
+});
+
+app.post('/authorize', (req, res) => {
+ console.log(PRE + $T() + '================== Handling / AUTHORIZE!');
+ let str = new String(req.body.password);
+ if (req.body.password === PASSWORD) {
+ res.cookie('nc-multiplex-auth', PASSWORD_HASH, {
+ maxAge: AUTH_MINUTES * 60 * 1000
+ }); // ms
+ res.redirect(`/manage`);
+ } else {
+ res.redirect(`/error_not_authorized`);
+ }
+});
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - -
+// HOME
+
+// HANDLE "/" -- HOME PAGE
+app.get('/', (req, res) => {
+ console.log(PRE + $T() + '================== Handling / ROOT!');
+ if (HOMEPAGE_EXISTS) {
+ res.sendFile(path.join(__dirname, 'home.html'));
+ } else {
+ res.set('Content-Type', 'text/html');
+ let response = logoHtml;
+ response += `
Please contact Professor Kalani Craig, Institute for Digital Arts & Humanities at (812) 856-5721 (BH) or craigkl@indiana.edu with questions or concerns and/or to request information contained on this website in an accessible format.
`;
+ res.send(response);
+ }
+});
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - -
+// HANDLE STATIC FILES
+//
+// Route Everything else to :3000
+// :3000 is a "BASE" app that is actually a full NetCreate app
+// but it does nothing but serve static files.
+//
+// This is necessary to catch static page requests that do not have
+// parameters, such as imports, requires, .js, .css, etc.
+//
+// This HAS to come LAST!
+//
+app.use(
+ createProxyMiddleware('/', {
+ target: `http://localhost:3000`,
+ ws: true,
+ changeOrigin: true
+ })
+);
+
+// ----------------------------------------------------------------------------
+
+// `request` parameters reference
+//
+// console.log(`\n\nREQUEST: ${req.originalUrl}`)
+// console.log("...pathname", pathname); // `/hawaii/`
+// console.log("...req.path", req.path); // '/'
+// console.log("...req.baseUrl", req.baseUrl); // '/hawaii'
+// console.log("...req.originalUrl", req.originalUrl); // '/hawaii/'
+// console.log("...req.params", req.params); // '{}'
+// console.log("...req.query", req.query); // '{}'
+// console.log("...req.route", req.route); // undefined
+// console.log("...req.hostname", req.hostname); // 'sub.localhost'
+// console.log("...req.subdomains", req.subdomains); // []
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// START PROXY
+
+app.listen(PORT_ROUTER, () => console.log(PRE, $T(), `running on port ${PORT_ROUTER}.`));
From 7db781c34313e5ba063170cc63eb78e02b82ff1b Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Mon, 19 Aug 2024 19:37:37 -0400
Subject: [PATCH 04/47] dev-sri/timestamps: clean-up nc-multiplex-sri so it's
clearer to see what it does
---
.gitignore | 6 +-
nc-launch-config.js | 2 +-
nc-launch-instance.js | 11 +-
nc-multiplex-sri.js | 815 +++++++++++++++++++-----------------------
package-lock.json | 80 ++++-
package.json | 3 +-
6 files changed, 461 insertions(+), 456 deletions(-)
diff --git a/.gitignore b/.gitignore
index e6f09e8..4b20d62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,8 @@
# ignore custom password file
# this should never be commited to the repo to expose the password
-SESAME
\ No newline at end of file
+SESAME
+
+# ignore digital ocean start script, logs
+do-start.sh
+log.txt
\ No newline at end of file
diff --git a/nc-launch-config.js b/nc-launch-config.js
index c27d70d..8d6a003 100755
--- a/nc-launch-config.js
+++ b/nc-launch-config.js
@@ -13,7 +13,7 @@ const path = require('node:path');
/// CONSTANTS & DECLARATIONS //////////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-const PRE = '...nc-launch-config:';
+const PRE = '_MUX_CNF -';
const SPC = ''.padStart(PRE.length, ' ');
/// REPO_PATHS is listed in order of precedence
/// if multiple matches are found, a warning will be emitted
diff --git a/nc-launch-instance.js b/nc-launch-instance.js
index 5a5c5c8..806db41 100755
--- a/nc-launch-instance.js
+++ b/nc-launch-instance.js
@@ -24,7 +24,7 @@ const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
-const PRE = '...nc-launch-instance:';
+const PRE = '_MUX_LN -';
function writeConfig(data) {
let script = NCUTILS.GetNCConfig(data);
@@ -41,13 +41,10 @@ function promiseServer(port) {
process.on('message', data => {
console.log(PRE,$T());
- console.log(PRE,$T(), 'STARTING DB', data.db);
- console.log(PRE,$T());
-
- console.log(PRE,$T(), '1. Setting netcreate-config.js.');
+ console.log(PRE, 'STARTING DB', data.db);
+ console.log(PRE, '1. Setting netcreate-config.js.');
writeConfig(data);
-
- console.log(PRE,$T(), '2. Starting server');
+ console.log(PRE, '2. Starting server');
startServer(data.port);
});
diff --git a/nc-multiplex-sri.js b/nc-multiplex-sri.js
index cf154a6..621fedc 100644
--- a/nc-multiplex-sri.js
+++ b/nc-multiplex-sri.js
@@ -1,62 +1,62 @@
-/*
+/*///////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\
- nc-multiplex.js
+ To start a new graph:
+ http://localhost/graph/tacitus/
- This creates a node-based proxy server that will
- spin up individual NetCreate graph instances
- running on their own node processes.
+ If the graph already exists, it will be loaded. Otherwise it will create a new graph.
+ You need to be logged into the manager for this to work.
- To start this manually:
- `node nc-multiplex.js`
+ Manager runs on `http://localhost:80`
- Or use `npm run start`
+ proxied routes
+ / => localhost:80 Root: NetCreate Manager page
+ /graph//#/edit/uid => localhost:3x00/#/edit/uid
+ /*.[js,css,html] => localhost:3000/net-lib.js
- Then go to `localhost` to view the manager.
- (NOTE: This runs on port 80, so need to add a port)
+ flags
- The manager will list the running databases.
+ node nc-multiplex.js --IP=192.168.1.40
+ node nc-multiplex.js --GOOGLEA=xxxxx
- To start a new graph:
- `http://localhost/graph/tacitus/`
- If the graph already exists, it will be loaded.
- Otherwise it will create a new graph.
- (You need to be logged into the manager for this
- to work.)
- Refresh the manager to view running databases.
-
-
- # Setting IP or Google Analytics code
-
- Use the optional `--ip` or `--googlea` parameters if you need
- to start the server with a specific IP address or google
- analytics code. e.g.:
-
- `node nc-multiplex.js --ip=192.168.1.40`
- `node nc-multiplex.js --googlea=xxxxx`
+\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/
+const { createProxyMiddleware } = require('http-proxy-middleware');
+const { fork, exec } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+const cookieParser = require('cookie-parser');
+const crypto = require('crypto');
+// session-related imports from netcreate subrepo
+const { NC_SERVER_PATH, NC_URL_CONFIG } = require('./nc-launch-config');
+const SESSION = require(`${NC_SERVER_PATH}/app/unisys/common-session.js`);
+//
+const NCUTILS = require('./modules/nc-utils.js');
+const NCLOG = require('./modules/nc-logging-utils');
- # Route Scheme
+/// API METHODS ///////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- / => localhost:80 Root: NetCreate Manager page
- /graph//#/edit/uid => localhost:3x00/#/edit/uid
- /*.[js,css,html] => localhost:3000/net-lib.js
+/// EXPORTS ///////////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/*/
- # Port scheme
+PORT SCHEME
The proxy server runs on port 80.
+
Defined in `port_router`
Base application port is 3000
Base websocket port is 4000
When the app is started, we initialize a pool of ports
- indices basedon the PROCESS_MAX value.
+ indices based on the PROCESS_MAX value.
- When a process is spawned, we grab from the pool of port
- indices, then generate new port numbers based on the
+ When a process is spawned, we grab from the pool of port indices, then generate new port numbers based on the
index, where the app port and the websocket (net) port share
the same basic index, e.g.
@@ -73,7 +73,7 @@
# netcreate-config.js / NC_CONFIG
NC_CONFIG is actually used by both the server-side scripts and
- client-side scripts to set the active database, ip, ports,
+ client-side scripts to set the active database, IP, ports,
netports, and google analytics code.
As such, it is generated twice:
@@ -89,170 +89,69 @@
REVIEW: The dynamically generated client-side version should probably be cached.
-*/
-
-///////////////////////////////////////////////////////////////////////////////
-//
-// CONSTANTS
+/*/
-const { createProxyMiddleware } = require('http-proxy-middleware');
-const { fork, exec } = require('child_process');
-const fs = require('fs');
-const path = require('path');
-const express = require('express');
-const cookieParser = require('cookie-parser');
-const crypto = require('crypto');
-
-// SRI HACK IN TIMESTAMP
-const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
-const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
-const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
-
-const app = express();
-app.use(express.urlencoded({ extended: true }));
-app.use(cookieParser());
-
-const NCUTILS = require('./modules/nc-utils.js');
-const { NC_SERVER_PATH, NC_URL_CONFIG } = require('./nc-launch-config');
-const PRE = '...nc-multiplex: '; // console.log prefix
-
-// SETTINGS
+/// CONSTANTS & DECLARATIONS //////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+PRE = 'NC_MUX -'; // console.log prefix, match length of netcreate output
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const PORT_ROUTER = 80;
const PORT_APP = 3000; // base port for nc apps
const PORT_WS = 4000; // base port for websockets
const DEFAULT_PASSWORD = 'kpop'; // override with SESAME file
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const PROCESS_MAX = 30; // Set this to limit the number of running processes
+const MEMORY_MIN = 256; // MB. Each node process is generally ~30 MB.
+const AUTO_NEW = false; // Set to true to allow auto-spawning a new database via url.
+const AUTH_MINUTES = 2; // Minutes. Number of minutes to authorize login cookie
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/// detected node version
+let NVMRC;
+/// command line flags
+const argv = require('minimist')(process.argv.slice(2));
+const GOOGLEA = argv['googlea'];
+const IP = argv['ip'];
+const m_port_pool = []; // array of available port indices, usu [1...100]
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
let HOMEPAGE_EXISTS; // Flag for existence of home.html override
let PASSWORD; // Either default password or password in `SESAME` file
let PASSWORD_HASH; // Hash generated from password
-let childProcesses = []; // array of forked process + meta info = { db, port, netport, portindex, process };
-
-// OPTIONS
-const PROCESS_MAX = 30; // Set this to limit the number of running processes
-// in order to keep a rein on CPU and MEM loads
-// If you set this higher than 100 you should make
-// sure you open inbound ports higher than 3100 and 4100
-
-const MEMORY_MIN = 256; // in MegaBytes
-// Don't start a new process if there is less than
-// MEMORY_MIN memory remaining.
-// In our testing with macOS and Ubuntu 18.04 on EC2:
-// * Each node process is generally ~30 MB.
-// * Servers stop responding with less than 100 MB remaining.
-
-const ALLOW_NEW = false; // default = false
-// false: App will respond with ERROR NO DATABASE if you enter
-// a url that points to a non-existent database.
-// true: Set to true to allow auto-spawning a new database via
-// url. e.g. going to `http://localhost/graph/newdb/`
-// would automatically create a new database if it
-// didn't already exist.
-
-const AUTH_MINUTES = 2; // default = 30
-// Number of minutes to authorize login cookie
-// After AUTH_MINUTES, the user wil have to re-login.
-
-// ----------------------------------------------------------------------------
-// check nvm version
-let NODE_VER;
-try {
- NODE_VER = fs.readFileSync('./.nvmrc', 'utf8').trim();
-} catch (err) {
- console.error('could not read .nvmrc', err);
- throw Error(`Could not read .nvmrc ${err}`);
-}
-exec('node --version', (error, stdout, stderr) => {
- if (stdout) {
- stdout = stdout.trim();
- if (stdout !== NODE_VER) {
- console.log('\x1b[97;41m');
- console.log(PRE, $T(), '*** NODE VERSION MISMATCH ***');
- console.log(PRE, $T(), '.. expected', NODE_VER, 'got', stdout);
- console.log(PRE, $T(), '.. did you remember to run nvm use?\x1b[0m');
- console.log('');
- }
- console.log(PRE, $T(), 'NODE VERSION:', stdout, 'OK');
- }
-});
-
-// ----------------------------------------------------------------------------
-// READ OPTIONAL ARGUMENTS
-//
-// To set ip address or google analytics code, call nc-multiplex with
-// arguments, e.g.
-//
-// `node nc-multiplex.js --ip=192.168.1.40`
-// `node nc-multiplex.js --googlea=xxxxx`
-//
-
-const argv = require('minimist')(process.argv.slice(2));
-const googlea = argv['googlea'];
-const ip = argv['ip'];
+let m_child_processes = []; // array of forked process + meta info = { db, port, netport, portindex, process };
-// ----------------------------------------------------------------------------
-// SET HOME PAGE OVERRIDE
-//
-// If there's a 'home.html' file, serve that at '/'.
-//
-try {
- fs.accessSync('home.html', fs.constants.R_OK);
- HOMEPAGE_EXISTS = true;
-} catch (err) {
- // no home page, use default
- HOMEPAGE_EXISTS = false;
-}
+/// SRI HACK IN TIMESTAMP /////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const { strDateStamp, strTimeStamp } = NCLOG;
+const $T = () => `${strDateStamp()} ${strTimeStamp()}`; // return timestamp string
-// ----------------------------------------------------------------------------
-// SET PASSWORD
-//
-// If there's a 'SESAME' file, use the password in there.
-// Otherwise, fallback to default.
-//
-try {
- let sesame = fs.readFileSync('SESAME', 'utf8');
- PASSWORD = sesame;
-} catch (err) {
- // no password, use default
- PASSWORD = DEFAULT_PASSWORD;
-}
-// Make Hash
-PASSWORD_HASH = GetHash(PASSWORD);
-
-///////////////////////////////////////////////////////////////////////////////
-//
-// UTILITIES
-
-/**
- * Number formatter
- * From stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
- * @param {integer} x
- * @return {string} Number formatted with commas, e.g. 123,456
+/// HELPER METHODS ////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns true if the db is currently running as a process
+ * @param {string} db - database name
*/
-function numberWithCommas(x) {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+function m_DatabaseIsRunning(db) {
+ return m_child_processes.find(route => route.db === db);
}
-/**
- * Returns true if the db is currently running as a process
- * @param {string} db
- */
-function DBIsRunning(db) {
- return childProcesses.find(route => route.db === db);
+/// UTILITY METHODS ///////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Number formatter - from stackoverflow.com/questions/2901102/ */
+function u_commas(x) {
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
-/**
- * Generates a list of tokens using the NetCreate commoon-session module
- * REVIEW: Requiring a module from the secondary netcreate-2018 repo
- * is a little iffy.
- * @param {string} clsId
- * @param {string} projId
- * @param {string} dataset
- * @param {integer} numGroups
- * @return {string}
+/// SESSION OPERATIONS ////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Generates a list of tokens using the NetCreate common-session module
+ * REVIEW: Requiring a module from the secondary netcreate-2018 repo
+ * is a little iffy.
+ * @param {string} clsId - classId
+ * @param {string} projId - projectId
+ * @param {string} dataset - database name
+ * @param {integer} numGroups - number of tokens to generate
+ * @return {string}
*/
function MakeToken(clsId, projId, dataset, numGroups) {
- const rpath = `${NC_SERVER_PATH}/app/unisys/common-session.js`;
- const SESSION = require(rpath);
// from nc-logic.js
if (typeof clsId !== 'string')
return 'args: str classId, str projId, str dataset, int numGroups';
@@ -274,20 +173,19 @@ function MakeToken(clsId, projId, dataset, numGroups) {
}
return out;
}
-
-/**
- * Used to generate a hashed password for use in the cookie
- * so that password text is not visible in the cookie.
- * @param {string} pw
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Used to generate a hashed password for use in the cookie
+ * so that password text is not visible in the cookie.
+ * @param {string} pw - plain text password
+ * @return {string} hash of password
*/
function GetHash(pw) {
let hash = crypto.createHash('sha1').update(pw).digest('hex');
return hash;
}
-
-/**
- * HASH is generated from the PASSWORD
- * @param {string} pw
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** HASH is generated from the PASSWORD
+ * @param {string} pw
*/
function CookieIsValid(req) {
if (!req || !req.cookies) return false;
@@ -296,60 +194,60 @@ function CookieIsValid(req) {
return pw === PASSWORD_HASH;
}
-///// PORT POOL ---------------------------------------------------------------
-
-// Initialize port pool
-// port 0 is for the base app
-const port_pool = []; // array of available port indices, usu [1...100]
+/// PORT POOLING //////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/// Initialize port pool - port 0 is for the base app
for (let i = 0; i <= PROCESS_MAX; i++) {
- port_pool.push(i);
+ m_port_pool.push(i);
}
-/**
- * Gets the next available port from the pool.
- *
- * @param {integer} index of route
- * @return {object} JSON object definition, e.g.
- * {
- * index: integer // e.g. 3
- * appport: integer // e.g. 3003
- * netport: integer // e.g. 4003
- * }
- * or `undefined` if no items are left in the pool
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Gets the next available port from the pool.
+ * @param {integer} index of route
+ * @return {object.index} index of port (eg. 3)
+ * @return {object.appport} port for app (e.g. 3003)
+ * @return {object.netport} port for websocket (e.g. 4003)
+ * or `undefined` if no items are left in the pool
*/
function PickPort() {
- if (port_pool.length < 1) return undefined;
- const index = port_pool.shift();
+ if (m_port_pool.length < 1) return undefined;
+ const index = m_port_pool.shift();
const result = {
index,
appport: PORT_APP + index,
netport: PORT_WS + index
};
+ // make sure that there are no duplicates in the pool
+ const dpool = Array.from(new Set(m_port_pool));
+ if (dpool.length !== m_port_pool.length) {
+ console.log(PRE,$T(), 'ERROR: Duplicate port indices in pool! This should not happen!');
+ console.log(PRE,$T(), 'Pool:', m_port_pool);
+ }
return result;
}
-/**
- *
- * @param {integer} index -- Port index to return to the pool
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns the port index to the pool
+ * @param {integer} index - Port index to return to the pool
*/
function ReleasePort(index) {
- if (port_pool.find(port => port === index))
- throw 'ERROR: Port already in pool! This should not happen! ' + index;
- port_pool.push(index);
+ if (m_port_pool.find(port => port === index)) {
+ console.log(PRE,$T(), 'ERROR: Port already in pool! This should not happen!', index);
+ // throw 'ERROR: Port already in pool! This should not happen! ' + index;
+ }
+ m_port_pool.push(index);
}
-/**
- * Returns true if there are no more port indices left in the pool
- * Used by /graph// route to check if it should spawn a new app
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns true if there are no more port indices left in the pool
+ * Used by /graph// route to check if it should spawn a new app
*/
function PortPoolIsEmpty() {
- return port_pool.length < 1;
+ return m_port_pool.length < 1;
}
-///////////////////////////////////////////////////////////////////////////////
-//
-// RENDERERS
-
+/// RENDERERS /////////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const logoHtml =
'
`;
- return response;
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Returns a list of databases in the runtime folder
- * formatted as HTML
s, with a link to open each graph.
- */
-function RenderDatabaseList() {
- let response = '
';
- let dbs = NCUTILS.GetDatabaseNamesArray();
- dbs.forEach(db => {
- // Don't list dbs that are already open
- if (!m_DatabaseIsRunning(db))
- response += `
`;
- return response;
-}
-
-/// PROCESS MANAGERS //////////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** API: Use this to spawn a new node instance via m_PromiseApp.
- * @param {string} db - dataset name
- * @return {integer} port to be used by router function
- * in app.use(`/graph/:graph/:file`...).
- */
-async function SpawnApp(db) {
- try {
- const newProcessDef = await m_PromiseApp(db);
- AddChildProcess(newProcessDef);
- SaveProcessState();
- return newProcessDef.port;
- } catch (err) {
- console.error(PRE + 'SpawnApp Failed with error', err);
- }
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** UTILITY: Promises a new node NetCreate application process. This forks
- * `nc-launch-instance.jssh`.
- *
- * To initiate the launch process, m_PromiseApp() uses process.send to
- * supply the parameters necessary to write its netcreate-config.js file
- * and start the server using the supplied ports.
- *
- * After the server has launched, the child process sends a message back
- * to report success or faiure.
- *
- * @param {string} db - dataset name to use
- * @resolve {object} sends the forked process and meta info
- */
-function m_PromiseApp(db) {
- return new Promise((resolve, reject) => {
- const ports = PickPort();
- if (ports === undefined) {
- reject(`Unable to find a free port. ${db} not created.`);
- }
- const { index, appport, netport } = ports;
- // 1. Define the fork
- const info = `${db}:${index}/${appport}/${netport}`; // ignored by launcher
- const forked = fork('./nc-launch-instance.jssh', [info]);
- // 2. Define fork success handler
- // When the child node process is up and running, it will
- // send a message back to this handler, which in turn
- // sends the new spec back to SpawnApp
- forked.on('message', msg => {
- const { event } = msg;
- if (event === 'SUCCESS') {
- console.log(
- PRE,
- $T(),
- `${CYN}instance confirmed '${db}' has launched (port ${appport})`,
- RST
- );
- const newProcessDef = {
- db,
- port: ports.appport,
- netport: ports.netport,
- portindex: ports.index,
- GOOGLEA: GOOGLEA,
- process: forked
- };
- resolve(newProcessDef); // pass to SpawnApp
- } else {
- console.log(PRE, `${RED}instance '${db}' failed to start`, RST);
- reject(`Failed to start instance '${db}'`);
- }
- });
-
- // 3. Send message to start fork
- // This sends the necessary startup prarameters to nc-start.js
- // When nc-start is completed, it will call the message
- // handler in #2 above
- const ncStartParams = {
- db,
- port: ports.appport,
- netport: ports.netport,
- process: forked,
- IP,
- GOOGLEA
- };
- console.log(
- PRE,
- `initializing launch of '${db}' on port:${ports.appport} netport:${ports.netport}`
- );
- forked.send(ncStartParams);
- });
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Add the newProcess to the array of m_child_processes
- * but only if it doesn't already exist
- * @param {object} route
- */
-function AddChildProcess(newProcess) {
- if (m_child_processes.find(route => route.db === newProcess.db)) return;
- m_child_processes.push(newProcess);
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Save the Process Datastructures from m_child_processes and m_proxy_pool
- * to a file. This is used to save the state of the multiplex server */
-function SaveProcessState() {
- const process_entries = m_child_processes.map(route => {
- return {
- db: route.db,
- port: route.port,
- netport: route.netport,
- portindex: route.portindex
- };
- });
- const ncmState = {
- child_processes: process_entries,
- proxy_pool: m_proxy_pool
- };
- fs.writeFileSync('.nc-process-state.json', JSON.stringify(ncmState));
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Restore Process Datastructures, bypassing the m_PromiseApp() process
- * Duplicated much of m_PromiseApp()
- */
-async function LoadProcessState(child_processes, proxy_pool) {
- console.log(PRE, `${GRNR} <<< RESTORING DATASETS <<< ${RST}`);
- for (route of child_processes) {
- const { db, port, netport, portindex } = route;
- const info = `${db}:${port}/${netport}`;
- console.log(PRE, `${GRN}<<< restarting '${db}' on ${info} ${RST}`);
- await new Promise((resolve, reject) => {
- const forked = fork('./nc-launch-instance.jssh', [info]);
- // define success handler
- forked.on('message', msg => {
- const { event } = msg;
- if (event === 'SUCCESS') {
- console.log(PRE, $T(), `${GRN}<<< RESTORED '${db}' on (port ${port})`, RST);
- route.process = forked;
- resolve();
- } else {
- console.log(PRE, `${RED}<<< RESTORE '${db}' failed`, RST);
- reject(`Failed to restart instance '${db}'`);
- }
- }); // end forked.on
- const ncStartParams = {
- db,
- port,
- netport,
- portindex,
- GOOGLEA: GOOGLEA,
- process: forked
- };
- forked.send(ncStartParams);
- }); // end promise
- } // end for
- m_proxy_pool = proxy_pool;
- m_child_processes = child_processes;
-}
-
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Used to check if we have enough memory to start a new node process
- * This is used to prevent node from starting too many processes.
- */
-function OutOfMemory() {
- let free = os.freemem() / 1024; // mb
- return free < MEMORY_MIN;
-}
-
-/*///////////////////////////// RUNTIME START \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\
-
-
- Start of Server Execution on Module Load
- - emit console header timestamp
- - check .nvmrc and node version
- - detect home page availability
- - read management password from SESAME file
-
-
-\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/
-
-/// RUNTIME: START LOGGING OUTPUT /////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-console.log(`\n\n\n`);
-console.log('-'.repeat(80));
-console.log(PRE, 'nc-multiplex started:', $T());
-console.log(PRE);
-
-/// RUNTIME: CHECK FOR BASE REPO //////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-const { primary, count } = ScanForRepos();
-if (primary === undefined) {
- console.log(PRE, `${RED}ERROR: no primary NetCreate repo found${RST}`);
- console.log(SPC, `Make sure you installed a repo to launch from.`);
- console.log(SPC, `See ${WARN}ReadMe.md${RST} for details.`);
- process.exit(1);
-}
-if (count === 1) {
- console.log(PRE, `reference subrepo: ${primary.repo}`);
-} else {
- console.log(
- PRE,
- `${WARN}WARNING: multiple NetCreate repos (${count}) found${RST}`
- );
- console.log(SPC, `defaulting to ${WARN}${primary.repo}${RST}`);
-}
-
-
-/// RUNTIME: CHECK NODE VERSION ///////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-try {
- NVMRC = fs.readFileSync('./.nvmrc', 'utf8').trim();
-} catch (err) {
- console.error('could not read .nvmrc', err);
- throw Error(`Could not read .nvmrc ${err}`);
-}
-exec('node --version', (error, stdout, stderr) => {
- if (stdout) {
- stdout = stdout.trim();
- if (stdout !== NVMRC) {
- console.log('\x1b[97;41m');
- console.log(PRE, '*** NODE VERSION MISMATCH ***');
- console.log(PRE, '.. expected', NVMRC, 'got', stdout);
- console.log(PRE, '.. did you remember to run nvm use?\x1b[0m');
- console.log('');
- }
- console.log(PRE, 'NODE VERSION:', stdout, 'OK');
- }
-});
-
-/// RUNTIME: DETECT CUSTOM HOME PAGE //////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-try {
- fs.accessSync('home.html', fs.constants.R_OK);
- HOMEPAGE_EXISTS = true;
-} catch (err) {
- // no home page, use default
- HOMEPAGE_EXISTS = false;
-}
-
-/// RUNTIME: SET PASSWORD /////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/// If 'SESAME' file exists, use the password in there instead of default
-try {
- let sesame = fs.readFileSync('SESAME', 'utf8');
- PASSWORD = sesame;
-} catch (err) {
- PASSWORD = DEFAULT_PASSWORD; // no password, use default
-}
-PASSWORD_HASH = GetHash(PASSWORD);
-
-/// RUNTIME: START HEARTBEAT TIMER ///////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-m_MemLog();
-setInterval(m_MemLog, HEARTBEAT * 60 * 1000); // log memory usage every X minutes
-
-/// EXPRESS CONFIGURATION /////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-// START BASE APP
-// This is needed to handle static file requests.
-// Most imports/requires do not specify the db route /graph/dbname/
-// so we need to provide a base app that responds to those static file
-// requests. This starts a generic "base" dataset at port 3000.
-
-const app = express();
-app.use(express.urlencoded({ extended: true }));
-app.use(cookieParser());
-
-/// EXPRESS DATA ACCESS ROUTES ////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** HANDLE /graph/:graph/netcreate-config.js
- * The config file needs to be dynamically served for each node instance,
- * otherwise they would share (and clobber) the same static file.
- * This route has to go before /graph/:graph/:file? below
- */
-app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
- const db = req.params.graph;
- let response = '';
- const child = m_child_processes.find(child => child.db === db);
- if (child) {
- console.log(
- PRE,
- $T(),
- `GET /graph/${child.db}/${NC_URL_CONFIG} (client ${req.ip})`
- );
- response += NCUTILS.GetNCConfig(child);
- } else {
- console.log(PRE, $T(), 'no graph-specific netcreate-config.js found for', db);
- response += 'ERROR: No database found to netcreate-config.js: ' + db;
- }
- res.set('Content-Type', 'application/javascript');
- res.send(response);
-});
-
-/// PROXY GRAPH REDIRECT //////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** CONFIG FUNCTION: m_RouterLogic used by http-proxy-middleware to route to
- * the correct port
- * @param {Express.Request} req The router function tries to route to the
- * correct port and path, checking whether it already exists. If flags allow,
- * it will spawn a new process if able to.
- */
-async function m_RouterLogic(req) {
- if (req.params === undefined) {
- console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
- console.log(PRE, $T(), 'req.ip:', req.ip);
- }
- const db = req.params.graph;
- let port;
- let path = '';
-
- // Authenticate to allow spawning
- let ALLOW_SPAWN = false;
- if (CookieIsValid(req)) {
- ALLOW_SPAWN = true;
- }
- // Is it already running?
- let route = m_child_processes.find(route => route.db === db);
- if (route) {
- // a) Yes. Use existing route!
- console.log(
- PRE,
- $T(),
- `>>> proxying request /graph/${route.db}:80 to :${route.port} (client ${req.ip})`
- );
- port = route.port;
- } else if (PortPoolIsEmpty()) {
- // b) No more ports available.
- console.log(PRE, $T(), '!!! no more ports. Not spawning', db);
- path = `/error_out_of_ports`;
- } else if (OutOfMemory()) {
- // c) Not enough memory to spawn new node instance
- console.log(PRE, $T(), '!!! out of memory. Not spawning', db);
- path = `/error_out_of_memory`;
- } else if (AUTO_NEW || ALLOW_SPAWN) {
- // c) Not defined yet, Create a new one.
- let reason = ALLOW_SPAWN ? 'spawn=true ' : 'spawn=false ';
- reason += AUTO_NEW ? 'new=true' : 'new=false';
- console.log(PRE, $T(), `*** auto spawning (${reason})`, db);
- port = await SpawnApp(db);
- } else {
- // c) Not defined or running, and not allowed to spawn
- console.log(
- PRE,
- $T(),
- `!!! /graph/${db} not allowed to spawn (AUTO_NEW=ALOW_SPAWN=false)`
- );
- path = `/error_no_database?graph=${db}`;
- }
- return {
- protocol: 'http:',
- host: 'localhost',
- port: port,
- path: path // if path is empty, it will be ignored
- };
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** CONFIG FUNCTION: m_ProxyFilter nominally rewrites the /graph/{db} to
- * localhost:{port}, but it also contains some debug code to detect if
- * req.params is undefined as we have seen this on our servers and are trying
- * to log the conditions when this happens.
- * @param {string} rpath - route to check (remainder after any params)
- * @param {Express.Request} req - request object
- */
-function m_ProxyFilter(rpath, req) {
- // sri debug detect if req.params is undefined
- if (req === undefined) {
- console.log(PRE, $T(), `??? USE ${route} req is undefined`);
- return false;
- }
- // detect if this is a websocket connection
- if (req.headers && req.headers.upgrade) {
- const { upgrade } = req.headers;
- if (typeof upgrade === 'string' && upgrade.toLowerCase() === 'websocket') {
- const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
- console.log(
- PRE,
- $T(),
- `${WARN}??? USE ${rpath} is a websocket connection attempt from ${ip}`,
- RST
- );
- }
- }
- if (req.params === undefined) {
- console.log(PRE, $T(), `${WARN}??? USE ${rpath} req.params is undefined`, RST);
- return false;
- }
- // pass if there is a file
- // (srinote: this param only contains the first segment, which may be a bug)
- if (req.params.file) return true; // only first segment of path (bug?)
- // pass if there is a trailing '/'
- if (req.params.graph && req.originalUrl.endsWith('/')) return true; // legit graph
- return false;
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** CONFIG FUNCTION: m_ProxyRewrite rewrites the path to remove the /graph/db/
- * prefix, used to reroute the calls to the correct port in the main proxy
- * middleware for /graph/dbname/ requests.
- */
-function m_ProxyRewrite(rpath, req) {
- // remove '/graph/db/' for the rerouted calls
- // e.g. localhost/graph/hawaii/#/edit/mop => localhost:3000/#/edit/mop
-
- const fullPath = req.originalUrl;
- if (req.originalUrl === undefined) {
- console.log(PRE, $T(), '??? ProxyRewrite req.originalUrl is undefined');
- return rpath;
- }
-
- /*/ srinote: in hpm 3, path is the remainder after the /graph/db/ prefix
- instead of the full path as before, so use req.originalUrl instead
- /*/
-
- // const rewrite = rpath.replace(`/graph/${req.params.graph}`, '');
- const rewrite = fullPath.replace(`/graph/${req.params.graph}/`, '/');
- return rewrite;
-}
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** MAIN HANDLER /graph/:graph/:file?
- * The intention is to proxy file requests from /graph/dbname/filename
- * to localhost:3000/filename (e.g. `netcreate-config.js` requests).
- */
-const proxy = createProxyMiddleware({
- // this is the actual proxy setup object
- router: m_RouterLogic,
- pathFilter: m_ProxyFilter,
- pathRewrite: m_ProxyRewrite,
- target: `http://localhost:3000`, // default fallback, router takes precedence
- ws: true,
- changeOrigin: true,
- on: {
- error: (err, req, res, target) => {
- console.log(PRE, $T(), '??? Proxy Error:', err);
- if (res.writeHead && !res.headersSent) {
- res.writeHead(500, {
- 'Content-Type': 'text/plain'
- });
- }
- res.end('Something went wrong with the proxy.');
- },
- close: (proxyRes, proxySocket, proxyHead) => {
- console.log(PRE, $T(), '??? Proxy client closed');
- }
- }
-});
-app.use('/graph/:graph/:file?', proxy);
-
-/// EXPRESS ERROR ROUTES //////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/// HANDLE NO DATABASE -- RETURN ERROR
-app.get('/error_no_database', (req, res) => {
- // get the ?graph value the query string
- let db = '';
- if (req.query && req.query.graph) db = req.query.graph;
- // overblown pretty-print formating
- if (db.endsWith('/')) db = db.slice(0, -1);
- db = db.length > 0 ? ` '${db}' ` : ' ';
- m_SendErrorResponse(res, `Requested graph${db}is not currently open`);
-});
-/// HANDLE NOT AUTHORIZED -- RETURN ERROR
-app.get('/error_not_authorized', (req, res) => {
- m_SendErrorResponse(res, 'Not Authorized.');
-});
-/// HANDLE OUT OF PORTS -- RETURN ERROR
-app.get('/error_out_of_ports', (req, res) => {
- m_SendErrorResponse(res, "Ran out of ports. Can't start the graph.");
-});
-/// HANDLE OUT OF MEMORY -- RETURN ERROR
-app.get('/error_out_of_memory', (req, res) => {
- m_SendErrorResponse(res, "Ran out of Memory. Can't start the graph.");
-});
-/// HANDLE MISSING TRAILING ".../" -- RETURN ERROR
-app.get('/graph/:file', (req, res) => {
- m_SendErrorResponse(res, "Bad URL. Missing trailing '/'.");
-});
-
-// EXPRESS UTILITY ROUTES /////////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-// HANDLE "/kill/:graph" -- KILL REQUEST
-app.get('/kill/:graph/', (req, res) => {
- if (req.params === undefined) {
- console.log(PRE, $T(), 'ERROR: req.params is undefined for /kill/:graph/');
- const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
- console.log(PRE, `error url: ${fullUrl}`);
- console.log(PRE, `client ip: ${req.ip}`);
- return;
- }
- const db = req.params ? req.params.graph : '';
- console.log(PRE, $T(), `GET /kill/${db} (client ${req.ip})`);
- res.set('Content-Type', 'text/html');
- let response = `
NetCreate Manager
`;
-
- const child = m_child_processes.find(child => child.db === db);
- if (child) {
- try {
- child.process.kill();
- // Return the port index to the pool
- ReleasePort(child.portindex);
- // Remove child from m_child_processes
- m_child_processes = m_child_processes.filter(child => child.db !== db);
- SaveProcessState(); // save state after updating process data structure
- console.log(PRE, $T(), `/kill/${db} process killed`);
- response += `
Process ${db} killed.
`;
- } catch (e) {
- console.log(PRE, $T(), `/kill/${db} process failed with ${e}`);
- response += `
ERROR while trying to kill ${db}
`;
- response += `
${e}
`;
- }
- } else {
- console.log(PRE, $T(), `/kill/${db} database not found`);
- response += 'ERROR: No database found to kill: ' + db;
- }
- response += `
`;
- res.send(response);
-});
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/// HANDLE "/maketoken" -- GENERATE TOKENS
-app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
- const { clsid, projid, dataset, numgroups } = req.params;
- console.log(
- PRE,
- $T(),
- 'maketoken GET on /maketoken',
- clsid,
- projid,
- dataset,
- numgroups
- );
- let response = MakeToken(clsid, projid, dataset, parseInt(numgroups));
- res.set('Content-Type', 'text/html');
- res.send(response);
-});
-
-/// EXPRESS MANAGEMENT ROUTES //////////////////////////////////////////////////
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - *\
- Authentication uses a cookie with a hashed password.
- The cookie expires after AUTH_MINUTES
-
- 1. /manage initially redirects to /login
- 2. On the /login form, the administrator enters a password
- 3. /login POSTS to /authorize
- 4. /authorize checks the password against the PASSWORD
- If there's no match, the user is redirected to /error_not_authorized
- 5. /authorize then sets a cookie with the PASSWORD_HASH and
- the user is redirected to /manage
- 6. /manage checks the cookie against the PASSWORD_HASH
- If the cookie matches, the manage page is displayed
- If the cookie doesn't match, the user is redirected back to /login
- 7. The cookie expires after AUTH_MINUTES
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-/// HANDLE MANAGER PAGE
-app.get('/manage', (req, res) => {
- console.log(PRE, $T(), `GET /manage (client ${req.ip})`);
- if (CookieIsValid(req)) {
- res.set('Content-Type', 'text/html');
- res.send(RenderManager());
- } else {
- res.redirect(`/login`);
- }
-});
-/// 2. redirected from /manage
-app.get('/login', (req, res) => {
- console.log(PRE, $T(), `GET /login (client ${req.ip})`);
- if (CookieIsValid(req)) {
- // Cookie already set, no need to log in, redirect to manage
- res.redirect(`/manage`);
- } else {
- // Show login form
- res.set('Content-Type', 'text/html');
- res.send(logoHtml + RenderLoginForm());
- }
-});
-/// 3. post from Login Form
-app.post('/authorize', (req, res) => {
- console.log(PRE, $T(), `POST /authorize (client ${req.ip})`);
- let str = new String(req.body.password);
- if (req.body.password === PASSWORD) {
- res.cookie('nc-multiplex-auth', PASSWORD_HASH, {
- maxAge: AUTH_MINUTES * 60 * 1000
- }); // ms
- res.redirect(`/manage`);
- } else {
- res.redirect(`/error_not_authorized`);
- }
-});
-
-/// EXPRESS HOME PAGE ROUTES //////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/// HANDLE "/" -- HOME PAGE
-app.get('/', (req, res) => {
- console.log(PRE, $T(), `GET / (client ${req.ip})`);
- if (HOMEPAGE_EXISTS) {
- console.log(PRE, '.. sending home.html');
- res.sendFile(path.join(__dirname, 'home.html'));
- } else {
- console.log(PRE, '.. no home.html, sending default');
- res.set('Content-Type', 'text/html');
- let response = logoHtml;
- response += `
Please contact Professor Kalani Craig, Institute for Digital Arts & Humanities at (812) 856-5721 (BH) or craigkl@indiana.edu with questions or concerns and/or to request information contained on this website in an accessible format.
`;
- res.send(response);
- }
-});
-
-/// EXPRESS STATIC FILE ROUTES /////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** Route Everything else to :3000
- * :3000 is a "BASE" app that is actually a full NetCreate app
- * but it does nothing but serve static files.
- *
- * This is necessary to catch static page requests that do not have
- * parameters, such as imports, requires, .js, .css, etc.
- *
- * This HAS to be the last route!
- */
-app.use(
- '/',
- createProxyMiddleware({
- target: `http://localhost:3000`,
- ws: true,
- changeOrigin: true
- })
-);
-
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-/** REQUEST PARAMETERS REFERENCE
- *
- console.log(`\n\nREQUEST: ${req.originalUrl}`)
- console.log("...pathname", pathname); // `/hawaii/`
- console.log("...req.path", req.path); // '/'
- console.log("...req.baseUrl", req.baseUrl); // '/hawaii'
- console.log("...req.originalUrl", req.originalUrl); // '/hawaii/'
- console.log("...req.params", req.params); // '{}'
- console.log("...req.query", req.query); // '{}'
- console.log("...req.route", req.route); // undefined
- console.log("...req.hostname", req.hostname); // 'sub.localhost'
- console.log("...req.subdomains", req.subdomains); // []
-**/
-
-/// EXPRESS START LISTENING ///////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-app.listen(PORT_ROUTER, () => {
- console.log(PRE, $T());
- console.log(PRE, `NC-MULTIPLEX Express Server running on port ${PORT_ROUTER}.`);
-
- // if .nc-process-state.json exists, read and parse it
- if (!fs.existsSync('.nc-process-state.json')) {
- console.log(PRE, 'No .nc-process-state.json found. Starting fresh.');
- SpawnApp('base');
- SaveProcessState();
- } else {
- try {
- const text = fs.readFileSync('.nc-process-state.json', 'utf8');
- const json = JSON.parse(text);
- const { child_processes, proxy_pool } = json;
- if (child_processes.length > 0) {
- LoadProcessState(child_processes, proxy_pool);
- } else {
- SpawnApp('base');
- SaveProcessState();
- }
- } catch (err) {
- console.log(PRE, $T(), 'error loading .nc-process-state.json', err);
- console.log(
- PRE,
- $T(),
- 'check contents of file, delete file, and restart manually'
- );
- process.exit(1);
- }
- }
-});
-
-/// PROCESS SIGNAL HANDLERS ///////////////////////////////////////////////////
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-process.on('SIGINT', () => {
- console.log(PRE, '*** SIGINT RECEIVED - EXITING ***');
- console.log(PRE, 'nc-multiplex stopped via SIGINT:', $T());
- process.exit(0);
-});
-/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-process.on('SIGTERM', () => {
- console.log(PRE, '*** SIGTERM RECEIVED - EXITING ***');
- console.log(PRE, 'nc-multiplex stopped via SIGTERM:', $T());
- process.exit(0);
-});
diff --git a/nc-multiplex.js b/nc-multiplex.js
index cf154a6..c93ed95 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -1,258 +1,182 @@
-/*
+/*///////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\
- nc-multiplex.js
+ NETCREATE MULTIPLEX SERVER - REMIX (2024)
+ Reformatted for debugging by Sri, so new bugs are mine :-)
- This creates a node-based proxy server that will
- spin up individual NetCreate graph instances
- running on their own node processes.
+ --- original comments ---
- To start this manually:
- `node nc-multiplex.js`
+ To start a new graph:
+ http://localhost/graph/tacitus/
- Or use `npm run start`
+ If the graph already exists, it will be loaded. Otherwise it will create a new graph.
+ You need to be logged into the manager for this to work.
- Then go to `localhost` to view the manager.
- (NOTE: This runs on port 80, so need to add a port)
+ Manager runs on `http://localhost:80`
- The manager will list the running databases.
+ proxied routes
+ / => localhost:80 Root: NetCreate Manager page
+ /graph//#/edit/uid => localhost:3x00/#/edit/uid
+ /*.[js,css,html] => localhost:3000/net-lib.js
- To start a new graph:
- `http://localhost/graph/tacitus/`
+ flags
- If the graph already exists, it will be loaded.
- Otherwise it will create a new graph.
- (You need to be logged into the manager for this
- to work.)
+ node nc-multiplex.js --IP=192.168.1.40
+ node nc-multiplex.js --GOOGLEA=xxxxx
- Refresh the manager to view running databases.
-
-
- # Setting IP or Google Analytics code
-
- Use the optional `--ip` or `--googlea` parameters if you need
- to start the server with a specific IP address or google
- analytics code. e.g.:
-
- `node nc-multiplex.js --ip=192.168.1.40`
- `node nc-multiplex.js --googlea=xxxxx`
-
-
- # Route Scheme
-
- / => localhost:80 Root: NetCreate Manager page
- /graph//#/edit/uid => localhost:3x00/#/edit/uid
- /*.[js,css,html] => localhost:3000/net-lib.js
-
-
- # Port scheme
-
- The proxy server runs on port 80.
- Defined in `port_router`
-
- Base application port is 3000
- Base websocket port is 4000
-
- When the app is started, we initialize a pool of ports
- indices basedon the PROCESS_MAX value.
-
- When a process is spawned, we grab from the pool of port
- indices, then generate new port numbers based on the
- index, where the app port and the websocket (net) port share
- the same basic index, e.g.
-
- {
- index: 2,
- appport: 3002,
- netport: 4002
- }
-
- When the process is killed, the port index is returned
- to the pool and re-used.
-
-
- # netcreate-config.js / NC_CONFIG
-
- NC_CONFIG is actually used by both the server-side scripts and
- client-side scripts to set the active database, ip, ports,
- netports, and google analytics code.
-
- As such, it is generated twice:
- 1. server-side: nc-start.js will generate a local file version into /build/app/assets
- where it is used by brunch-server.js, brunch-config.js, and server-database.js
- during the app start process.
- 2. client-side: nc-multiplex.js will then dynamically generate netcreate-config.js
- for each graph's http request.
-
- REVIEW: There is a potential conflict server-side if two graphs
- are started up at the same time and the newly generated netcreate-config.js
- files cross each other.
-
- REVIEW: The dynamically generated client-side version should probably be cached.
-
-*/
-
-///////////////////////////////////////////////////////////////////////////////
-//
-// CONSTANTS
+\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/
const { createProxyMiddleware } = require('http-proxy-middleware');
-const { fork, exec } = require('child_process');
+const { fork, exec, execSync } = require('child_process');
+const os = require('os');
const fs = require('fs');
const path = require('path');
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
-
-// SRI HACK IN TIMESTAMP
-const {strDateStamp, strTimeStamp} = require('./modules/nc-logging-utils');
-const TSTART = `${strDateStamp()} ${strTimeStamp()}`; // Start time
-const $T=()=>`${strDateStamp()} ${strTimeStamp()}`; // Update time
-
-const app = express();
-app.use(express.urlencoded({ extended: true }));
-app.use(cookieParser());
-
+// session-related imports from netcreate subrepo
+const { NC_SERVER_PATH, NC_URL_CONFIG, ScanForRepos } = require('./nc-launch-config');
+const SESSION = require(`${NC_SERVER_PATH}/app/unisys/common-session.js`);
+//
const NCUTILS = require('./modules/nc-utils.js');
-const { NC_SERVER_PATH, NC_URL_CONFIG } = require('./nc-launch-config');
-const PRE = '...nc-multiplex: '; // console.log prefix
+const NCLOG = require('./modules/nc-logging-utils');
-// SETTINGS
+/// CONSTANTS & DECLARATIONS //////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const PRE = 'NC_MUX -'; // console.log prefix, match length of netcreate output
+const SPC = ' '.repeat(PRE.length);
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const PORT_ROUTER = 80;
const PORT_APP = 3000; // base port for nc apps
const PORT_WS = 4000; // base port for websockets
const DEFAULT_PASSWORD = 'kpop'; // override with SESAME file
-
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const PROCESS_MAX = 30; // Set this to limit the number of running processes
+const MEMORY_MIN = 256; // MB. Each node process is generally ~30 MB.
+const AUTO_NEW = false; // Set to true to allow auto-spawning a new database via url.
+const AUTH_MINUTES = 2; // Minutes. Number of minutes to authorize login cookie
+const HEARTBEAT = 15; // Minutes. Number of minutes between memory log heartbeats
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/// detected node version
+let NVMRC;
+/// command line flags
+const argv = require('minimist')(process.argv.slice(2));
+const GOOGLEA = argv['googlea'];
+const IP = argv['ip'];
+/// local data structures
+let m_proxy_pool = []; // array of available port indices, usu [1...100]
+let m_child_processes = []; // array of forked process + meta info = { db, port, netport, portindex, process };
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
let HOMEPAGE_EXISTS; // Flag for existence of home.html override
let PASSWORD; // Either default password or password in `SESAME` file
let PASSWORD_HASH; // Hash generated from password
-let childProcesses = []; // array of forked process + meta info = { db, port, netport, portindex, process };
-
-// OPTIONS
-const PROCESS_MAX = 30; // Set this to limit the number of running processes
-// in order to keep a rein on CPU and MEM loads
-// If you set this higher than 100 you should make
-// sure you open inbound ports higher than 3100 and 4100
-
-const MEMORY_MIN = 256; // in MegaBytes
-// Don't start a new process if there is less than
-// MEMORY_MIN memory remaining.
-// In our testing with macOS and Ubuntu 18.04 on EC2:
-// * Each node process is generally ~30 MB.
-// * Servers stop responding with less than 100 MB remaining.
-
-const ALLOW_NEW = false; // default = false
-// false: App will respond with ERROR NO DATABASE if you enter
-// a url that points to a non-existent database.
-// true: Set to true to allow auto-spawning a new database via
-// url. e.g. going to `http://localhost/graph/newdb/`
-// would automatically create a new database if it
-// didn't already exist.
-
-const AUTH_MINUTES = 2; // default = 30
-// Number of minutes to authorize login cookie
-// After AUTH_MINUTES, the user wil have to re-login.
-
-// ----------------------------------------------------------------------------
-// check nvm version
-let NODE_VER;
-try {
- NODE_VER = fs.readFileSync('./.nvmrc', 'utf8').trim();
-} catch (err) {
- console.error('could not read .nvmrc', err);
- throw Error(`Could not read .nvmrc ${err}`);
-}
-exec('node --version', (error, stdout, stderr) => {
- if (stdout) {
- stdout = stdout.trim();
- if (stdout !== NODE_VER) {
- console.log('\x1b[97;41m');
- console.log(PRE, $T(), '*** NODE VERSION MISMATCH ***');
- console.log(PRE, $T(), '.. expected', NODE_VER, 'got', stdout);
- console.log(PRE, $T(), '.. did you remember to run nvm use?\x1b[0m');
- console.log('');
- }
- console.log(PRE, $T(), 'NODE VERSION:', stdout, 'OK');
- }
-});
-
-// ----------------------------------------------------------------------------
-// READ OPTIONAL ARGUMENTS
-//
-// To set ip address or google analytics code, call nc-multiplex with
-// arguments, e.g.
-//
-// `node nc-multiplex.js --ip=192.168.1.40`
-// `node nc-multiplex.js --googlea=xxxxx`
-//
-
-const argv = require('minimist')(process.argv.slice(2));
-const googlea = argv['googlea'];
-const ip = argv['ip'];
-
-// ----------------------------------------------------------------------------
-// SET HOME PAGE OVERRIDE
-//
-// If there's a 'home.html' file, serve that at '/'.
-//
-try {
- fs.accessSync('home.html', fs.constants.R_OK);
- HOMEPAGE_EXISTS = true;
-} catch (err) {
- // no home page, use default
- HOMEPAGE_EXISTS = false;
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const CYN = '\x1b[96m'; // cyan
+const CYNR = '\x1b[46m'; // reversed cyan
+const GRN = '\x1b[92m'; // green
+const GRNR = '\x1b[42m'; // green reversed
+const RST = '\x1b[0m'; // reset
+const RED = '\x1b[91m'; // red
+const WARN = '\x1b[93m'; // yellow
+
+/// SRI HACK IN TIMESTAMP /////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const { strDateStamp, strTimeStamp } = NCLOG;
+const $T = () => `${strDateStamp()} ${strTimeStamp()}`; // return timestamp string
+
+/// HELPER METHODS ////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** return memory parameters */
+function m_MemoryReport(unit = 'kb') {
+ const _fmt = x => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ let cf;
+ if (unit === 'kb') cf = 1024;
+ if (unit === 'mb') cf = 1024 * 1024;
+ const { heapUsed, heapTotal } = process.memoryUsage();
+ const huse = _fmt(Math.trunc(heapUsed / cf));
+ const htot = _fmt(Math.trunc(heapTotal / cf));
+ const hpct = (100 * (heapUsed / heapTotal)).toFixed(2);
+ const hrem = _fmt(Math.trunc((heapTotal - heapUsed) / cf));
+ const kb2mb = 1024 * 1024;
+ const sysTotal = _fmt(Math.trunc(os.totalmem() / kb2mb));
+ const sysFree = _fmt(Math.trunc(os.freemem() / kb2mb));
+
+ const pids = m_GetInstancePIDs();
+ return {
+ unit,
+ heapUsed: huse,
+ heapTotal: htot,
+ heapPercent: hpct,
+ heapBuffer: hrem,
+ pids,
+ sysTotalMB: sysTotal,
+ sysFreeMB: sysFree
+ };
}
-
-// ----------------------------------------------------------------------------
-// SET PASSWORD
-//
-// If there's a 'SESAME' file, use the password in there.
-// Otherwise, fallback to default.
-//
-try {
- let sesame = fs.readFileSync('SESAME', 'utf8');
- PASSWORD = sesame;
-} catch (err) {
- // no password, use default
- PASSWORD = DEFAULT_PASSWORD;
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** periodically log memory usage and running instances to console */
+function m_MemLog() {
+ let { heapUsed, heapTotal, heapPercent, sysFreeMB, unit, pids } = m_MemoryReport();
+ console.log(
+ PRE,
+ '* MEMORY HEARTBEAT',
+ $T(),
+ `- nodeHeap ${heapUsed} / ${heapTotal}${unit} (${heapPercent}%)`,
+ `- freeMem ${sysFreeMB}mb`
+ );
+ const out = pids.split('\n');
+ if (out.length > 1)
+ out.forEach(line => {
+ if (line.trim().length > 0) console.log(PRE, '*', line.trim());
+ });
}
-// Make Hash
-PASSWORD_HASH = GetHash(PASSWORD);
-
-///////////////////////////////////////////////////////////////////////////////
-//
-// UTILITIES
-
-/**
- * Number formatter
- * From stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
- * @param {integer} x
- * @return {string} Number formatted with commas, e.g. 123,456
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns true if the db is currently running as a process
+ * @param {string} db - database name
*/
-function numberWithCommas(x) {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+function m_DatabaseIsRunning(db) {
+ return m_child_processes.find(route => route.db === db);
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** get the output of ps matching nc-launch-instance.jssh entries */
+function m_GetInstancePIDs() {
+ const stdout = execSync('ps -e -o pid -o command | grep nc-launch-instance.jssh');
+ const regex = /(\d+).+\/versions\/node\/(.+)/;
+ const lines = stdout.toString().split('\n');
+ let out = '';
+ lines.forEach(line => {
+ if (line.includes('/bin/node ./nc-launch-instance.jssh')) {
+ const match = line.match(regex);
+ if (match) {
+ const [_, pid, cli] = match;
+ const paddedPid = pid.padEnd(7, ' ');
+ out += ` ${paddedPid} .nvm/versions/${cli}\n`;
+ }
+ }
+ });
+ return out;
+}
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** express helper to return an error */
+function m_SendErrorResponse(res, msg) {
+ res.set('Content-Type', 'text/html');
+ res.send(
+ `
`
+ );
}
-/**
- * Returns true if the db is currently running as a process
- * @param {string} db
- */
-function DBIsRunning(db) {
- return childProcesses.find(route => route.db === db);
-}
-
-/**
- * Generates a list of tokens using the NetCreate commoon-session module
- * REVIEW: Requiring a module from the secondary netcreate-2018 repo
- * is a little iffy.
- * @param {string} clsId
- * @param {string} projId
- * @param {string} dataset
- * @param {integer} numGroups
- * @return {string}
+/// SESSION OPERATIONS ////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Generates a list of tokens using the NetCreate common-session module
+ * REVIEW: Requiring a module from the secondary netcreate-2018 repo
+ * is a little iffy.
+ * @param {string} clsId - classId
+ * @param {string} projId - projectId
+ * @param {string} dataset - database name
+ * @param {integer} numGroups - number of tokens to generate
+ * @return {string}
*/
function MakeToken(clsId, projId, dataset, numGroups) {
- const rpath = `${NC_SERVER_PATH}/app/unisys/common-session.js`;
- const SESSION = require(rpath);
// from nc-logic.js
if (typeof clsId !== 'string')
return 'args: str classId, str projId, str dataset, int numGroups';
@@ -274,20 +198,19 @@ function MakeToken(clsId, projId, dataset, numGroups) {
}
return out;
}
-
-/**
- * Used to generate a hashed password for use in the cookie
- * so that password text is not visible in the cookie.
- * @param {string} pw
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Used to generate a hashed password for use in the cookie
+ * so that password text is not visible in the cookie.
+ * @param {string} pw - plain text password
+ * @return {string} hash of password
*/
function GetHash(pw) {
let hash = crypto.createHash('sha1').update(pw).digest('hex');
return hash;
}
-
-/**
- * HASH is generated from the PASSWORD
- * @param {string} pw
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** HASH is generated from the PASSWORD
+ * @param {string} pw
*/
function CookieIsValid(req) {
if (!req || !req.cookies) return false;
@@ -296,60 +219,80 @@ function CookieIsValid(req) {
return pw === PASSWORD_HASH;
}
-///// PORT POOL ---------------------------------------------------------------
-
-// Initialize port pool
-// port 0 is for the base app
-const port_pool = []; // array of available port indices, usu [1...100]
+/// PORT POOLING //////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** The proxy server runs on port 80, hosting various management routes as well
+ * as the /graph// proxying
+ *
+ * - Base application port is 3000
+ * - Base websocket port is 4000
+ *
+ * Launched NetCreate instances are reserved by m_proxy_pool. The entities
+ * in m_proxy_pool range from 0...PROCESS_MAX, are stored as offsets from the
+ * base ports. When an instance is killed through the management UI, the
+ * port index is returned to the pool. The UI prevents the base app from being
+ * reallocated.
+ */
for (let i = 0; i <= PROCESS_MAX; i++) {
- port_pool.push(i);
+ m_proxy_pool.push(i);
}
-/**
- * Gets the next available port from the pool.
- *
- * @param {integer} index of route
- * @return {object} JSON object definition, e.g.
- * {
- * index: integer // e.g. 3
- * appport: integer // e.g. 3003
- * netport: integer // e.g. 4003
- * }
- * or `undefined` if no items are left in the pool
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Gets the next available port from the pool.
+ * @param {integer} index of route
+ * @return {object.index} index of port (eg. 3)
+ * @return {object.appport} port for app (e.g. 3003)
+ * @return {object.netport} port for websocket (e.g. 4003)
+ * or `undefined` if no items are left in the pool
*/
function PickPort() {
- if (port_pool.length < 1) return undefined;
- const index = port_pool.shift();
+ if (m_proxy_pool.length < 1) return undefined;
+ const index = m_proxy_pool.shift();
const result = {
index,
appport: PORT_APP + index,
netport: PORT_WS + index
};
+ // make sure that there are no duplicates in the pool
+ const dpool = Array.from(new Set(m_proxy_pool));
+ if (dpool.length !== m_proxy_pool.length) {
+ console.log(
+ PRE,
+ $T(),
+ 'ERROR: Duplicate port indices in pool! This should not happen!'
+ );
+ console.log(PRE, $T(), 'Pool:', m_proxy_pool);
+ }
return result;
}
-/**
- *
- * @param {integer} index -- Port index to return to the pool
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns the port index to the pool
+ * @param {integer} index - Port index to return to the pool
*/
function ReleasePort(index) {
- if (port_pool.find(port => port === index))
- throw 'ERROR: Port already in pool! This should not happen! ' + index;
- port_pool.push(index);
+ if (m_proxy_pool.find(port => port === index)) {
+ console.log(
+ PRE,
+ $T(),
+ 'ERROR: Port already in pool! This should not happen!',
+ index
+ );
+ // throw 'ERROR: Port already in pool! This should not happen! ' + index;
+ }
+ m_proxy_pool.push(index);
}
-/**
- * Returns true if there are no more port indices left in the pool
- * Used by /graph// route to check if it should spawn a new app
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/** Returns true if there are no more port indices left in the pool
+ * Used by /graph// route to check if it should spawn a new app
*/
function PortPoolIsEmpty() {
- return port_pool.length < 1;
+ return m_proxy_pool.length < 1;
}
-///////////////////////////////////////////////////////////////////////////////
-//
-// RENDERERS
-
+/// RENDERERS /////////////////////////////////////////////////////////////////
+/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const logoHtml =
'
`;
return response;
}
@@ -626,7 +642,7 @@ async function LoadProcessState(child_processes, proxy_pool) {
*/
function OutOfMemory() {
let free = os.freemem() / 1024; // mb
- return free < MEMORY_MIN;
+ return free < SYSMEM_MIN;
}
/*///////////////////////////// RUNTIME START \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\
@@ -643,9 +659,16 @@ function OutOfMemory() {
/// RUNTIME: START LOGGING OUTPUT /////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+const m_stat = {
+ start: '', // timestamp of server start
+ refreshed: '' // timestamp of last refresh
+};
+m_stat.start = $T();
+fs.writeFileSync('.nc-server-start.txt', m_stat.start);
+///
console.log(`\n\n\n`);
console.log('-'.repeat(80));
-console.log(PRE, 'nc-multiplex started:', $T());
+console.log(PRE, 'nc-multiplex started:', m_stat.start);
console.log(PRE);
/// RUNTIME: CHECK FOR BASE REPO //////////////////////////////////////////////
@@ -660,14 +683,10 @@ if (primary === undefined) {
if (count === 1) {
console.log(PRE, `reference subrepo: ${primary.repo}`);
} else {
- console.log(
- PRE,
- `${WARN}WARNING: multiple NetCreate repos (${count}) found${RST}`
- );
+ console.log(PRE, `${WARN}WARNING: multiple NetCreate repos (${count}) found${RST}`);
console.log(SPC, `defaulting to ${WARN}${primary.repo}${RST}`);
}
-
/// RUNTIME: CHECK NODE VERSION ///////////////////////////////////////////////
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
try {
@@ -736,7 +755,7 @@ app.use(cookieParser());
* This route has to go before /graph/:graph/:file? below
*/
app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
- const db = req.params.graph;
+ const db = STAT.graph;
let response = '';
const child = m_child_processes.find(child => child.db === db);
if (child) {
@@ -763,11 +782,11 @@ app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
* it will spawn a new process if able to.
*/
async function m_RouterLogic(req) {
- if (req.params === undefined) {
- console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
+ if (STAT === undefined) {
+ console.log(PRE, $T(), 'ERROR in m_RouterLogic: STAT is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
}
- const db = req.params.graph;
+ const db = STAT.graph;
let port;
let path = '';
@@ -819,13 +838,13 @@ async function m_RouterLogic(req) {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/** CONFIG FUNCTION: m_ProxyFilter nominally rewrites the /graph/{db} to
* localhost:{port}, but it also contains some debug code to detect if
- * req.params is undefined as we have seen this on our servers and are trying
+ * STAT is undefined as we have seen this on our servers and are trying
* to log the conditions when this happens.
* @param {string} rpath - route to check (remainder after any params)
* @param {Express.Request} req - request object
*/
function m_ProxyFilter(rpath, req) {
- // sri debug detect if req.params is undefined
+ // sri debug detect if STAT is undefined
if (req === undefined) {
console.log(PRE, $T(), `??? USE ${route} req is undefined`);
return false;
@@ -843,15 +862,15 @@ function m_ProxyFilter(rpath, req) {
);
}
}
- if (req.params === undefined) {
- console.log(PRE, $T(), `${WARN}??? USE ${rpath} req.params is undefined`, RST);
+ if (STAT === undefined) {
+ console.log(PRE, $T(), `${WARN}??? USE ${rpath} STAT is undefined`, RST);
return false;
}
// pass if there is a file
// (srinote: this param only contains the first segment, which may be a bug)
- if (req.params.file) return true; // only first segment of path (bug?)
+ if (STAT.file) return true; // only first segment of path (bug?)
// pass if there is a trailing '/'
- if (req.params.graph && req.originalUrl.endsWith('/')) return true; // legit graph
+ if (STAT.graph && req.originalUrl.endsWith('/')) return true; // legit graph
return false;
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -873,8 +892,8 @@ function m_ProxyRewrite(rpath, req) {
instead of the full path as before, so use req.originalUrl instead
/*/
- // const rewrite = rpath.replace(`/graph/${req.params.graph}`, '');
- const rewrite = fullPath.replace(`/graph/${req.params.graph}/`, '/');
+ // const rewrite = rpath.replace(`/graph/${STAT.graph}`, '');
+ const rewrite = fullPath.replace(`/graph/${STAT.graph}/`, '/');
return rewrite;
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -940,14 +959,14 @@ app.get('/graph/:file', (req, res) => {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// HANDLE "/kill/:graph" -- KILL REQUEST
app.get('/kill/:graph/', (req, res) => {
- if (req.params === undefined) {
- console.log(PRE, $T(), 'ERROR: req.params is undefined for /kill/:graph/');
+ if (STAT === undefined) {
+ console.log(PRE, $T(), 'ERROR: STAT is undefined for /kill/:graph/');
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
console.log(PRE, `error url: ${fullUrl}`);
console.log(PRE, `client ip: ${req.ip}`);
return;
}
- const db = req.params ? req.params.graph : '';
+ const db = STAT ? STAT.graph : '';
console.log(PRE, $T(), `GET /kill/${db} (client ${req.ip})`);
res.set('Content-Type', 'text/html');
let response = `
NetCreate Manager
`;
@@ -978,7 +997,7 @@ app.get('/kill/:graph/', (req, res) => {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// HANDLE "/maketoken" -- GENERATE TOKENS
app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
- const { clsid, projid, dataset, numgroups } = req.params;
+ const { clsid, projid, dataset, numgroups } = STAT;
console.log(
PRE,
$T(),
@@ -1012,8 +1031,12 @@ app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/// HANDLE MANAGER PAGE
app.get('/manage', (req, res) => {
+ m_stat.refreshed = new Date();
console.log(PRE, $T(), `GET /manage (client ${req.ip})`);
if (CookieIsValid(req)) {
+ res.cookie('nc-multiplex-auth', PASSWORD_HASH, {
+ maxAge: AUTH_MINUTES * 60 * 1000
+ }); // ms
res.set('Content-Type', 'text/html');
res.send(RenderManager());
} else {
@@ -1091,7 +1114,7 @@ app.use(
console.log("...req.path", req.path); // '/'
console.log("...req.baseUrl", req.baseUrl); // '/hawaii'
console.log("...req.originalUrl", req.originalUrl); // '/hawaii/'
- console.log("...req.params", req.params); // '{}'
+ console.log("...STAT", STAT); // '{}'
console.log("...req.query", req.query); // '{}'
console.log("...req.route", req.route); // undefined
console.log("...req.hostname", req.hostname); // 'sub.localhost'
@@ -1136,12 +1159,14 @@ app.listen(PORT_ROUTER, () => {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
process.on('SIGINT', () => {
console.log(PRE, '*** SIGINT RECEIVED - EXITING ***');
+ console.log(PRE);
console.log(PRE, 'nc-multiplex stopped via SIGINT:', $T());
process.exit(0);
});
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
process.on('SIGTERM', () => {
console.log(PRE, '*** SIGTERM RECEIVED - EXITING ***');
+ console.log(PRE);
console.log(PRE, 'nc-multiplex stopped via SIGTERM:', $T());
process.exit(0);
});
From c9471ee9111db30d92d8f771f4e8b65c59da7cc6 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 00:49:04 -0400
Subject: [PATCH 36/47] timestamps: fix weird change of "req.params'" to "STAT"
---
nc-multiplex.js | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 4f71824..516d25c 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -755,7 +755,7 @@ app.use(cookieParser());
* This route has to go before /graph/:graph/:file? below
*/
app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
- const db = STAT.graph;
+ const db = req.params.graph;
let response = '';
const child = m_child_processes.find(child => child.db === db);
if (child) {
@@ -782,11 +782,11 @@ app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
* it will spawn a new process if able to.
*/
async function m_RouterLogic(req) {
- if (STAT === undefined) {
- console.log(PRE, $T(), 'ERROR in m_RouterLogic: STAT is undefined');
+ if (req.params === undefined) {
+ console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
}
- const db = STAT.graph;
+ const db = req.params.graph;
let port;
let path = '';
@@ -838,13 +838,13 @@ async function m_RouterLogic(req) {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/** CONFIG FUNCTION: m_ProxyFilter nominally rewrites the /graph/{db} to
* localhost:{port}, but it also contains some debug code to detect if
- * STAT is undefined as we have seen this on our servers and are trying
+ * req.params is undefined as we have seen this on our servers and are trying
* to log the conditions when this happens.
* @param {string} rpath - route to check (remainder after any params)
* @param {Express.Request} req - request object
*/
function m_ProxyFilter(rpath, req) {
- // sri debug detect if STAT is undefined
+ // sri debug detect if req.params is undefined
if (req === undefined) {
console.log(PRE, $T(), `??? USE ${route} req is undefined`);
return false;
@@ -862,15 +862,15 @@ function m_ProxyFilter(rpath, req) {
);
}
}
- if (STAT === undefined) {
- console.log(PRE, $T(), `${WARN}??? USE ${rpath} STAT is undefined`, RST);
+ if (req.params === undefined) {
+ console.log(PRE, $T(), `${WARN}??? USE ${rpath} req.params is undefined`, RST);
return false;
}
// pass if there is a file
// (srinote: this param only contains the first segment, which may be a bug)
- if (STAT.file) return true; // only first segment of path (bug?)
+ if (req.params.file) return true; // only first segment of path (bug?)
// pass if there is a trailing '/'
- if (STAT.graph && req.originalUrl.endsWith('/')) return true; // legit graph
+ if (req.params.graph && req.originalUrl.endsWith('/')) return true; // legit graph
return false;
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -892,8 +892,8 @@ function m_ProxyRewrite(rpath, req) {
instead of the full path as before, so use req.originalUrl instead
/*/
- // const rewrite = rpath.replace(`/graph/${STAT.graph}`, '');
- const rewrite = fullPath.replace(`/graph/${STAT.graph}/`, '/');
+ // const rewrite = rpath.replace(`/graph/${req.params.graph}`, '');
+ const rewrite = fullPath.replace(`/graph/${req.params.graph}/`, '/');
return rewrite;
}
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -959,14 +959,14 @@ app.get('/graph/:file', (req, res) => {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// HANDLE "/kill/:graph" -- KILL REQUEST
app.get('/kill/:graph/', (req, res) => {
- if (STAT === undefined) {
- console.log(PRE, $T(), 'ERROR: STAT is undefined for /kill/:graph/');
+ if (req.params === undefined) {
+ console.log(PRE, $T(), 'ERROR: req.params is undefined for /kill/:graph/');
const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
console.log(PRE, `error url: ${fullUrl}`);
console.log(PRE, `client ip: ${req.ip}`);
return;
}
- const db = STAT ? STAT.graph : '';
+ const db = req.params ? req.params.graph : '';
console.log(PRE, $T(), `GET /kill/${db} (client ${req.ip})`);
res.set('Content-Type', 'text/html');
let response = `
NetCreate Manager
`;
@@ -997,7 +997,7 @@ app.get('/kill/:graph/', (req, res) => {
/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// HANDLE "/maketoken" -- GENERATE TOKENS
app.get('/maketoken/:clsid/:projid/:dataset/:numgroups', (req, res) => {
- const { clsid, projid, dataset, numgroups } = STAT;
+ const { clsid, projid, dataset, numgroups } = req.params;
console.log(
PRE,
$T(),
@@ -1114,7 +1114,7 @@ app.use(
console.log("...req.path", req.path); // '/'
console.log("...req.baseUrl", req.baseUrl); // '/hawaii'
console.log("...req.originalUrl", req.originalUrl); // '/hawaii/'
- console.log("...STAT", STAT); // '{}'
+ console.log("...req.params", req.params); // '{}'
console.log("...req.query", req.query); // '{}'
console.log("...req.route", req.route); // undefined
console.log("...req.hostname", req.hostname); // 'sub.localhost'
From 65ede3a52c4bcadb974f8dfb7a1ce79c2c447809 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 13:37:48 -0400
Subject: [PATCH 37/47] timestamps: rename start.sh to start-nc-multiplex.sh so
it's clearer when using pm2
avoid absurdist commands that look like "pm2 stop start"
---
start.sh => start-nc-multiplex.sh | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename start.sh => start-nc-multiplex.sh (100%)
diff --git a/start.sh b/start-nc-multiplex.sh
similarity index 100%
rename from start.sh
rename to start-nc-multiplex.sh
From 95af812ab3bf8673c039fc991bfe07b1979324a1 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 13:45:15 -0400
Subject: [PATCH 38/47] timestamps: add pm2 instructions to the
start-nc-multiplex.sh comments
---
start-nc-multiplex.sh | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/start-nc-multiplex.sh b/start-nc-multiplex.sh
index 0384ad2..eb04630 100755
--- a/start-nc-multiplex.sh
+++ b/start-nc-multiplex.sh
@@ -4,10 +4,19 @@
# the node process is restarted if it crashes, though you can
# also run it directly from the command line.
+# the script is often run using the pm2 process manager.
+# [app] is start-nc-multiplex.sh
+# pm2 list
+# pm2 stop [app] # stop the process in the pm2 list
+# pm2 delete [app] # to remove from pm2 list
+# pm2 save # to save the current list of processes
+# pm2 start [app] # to start the process
+
printf "starting nc-multiplex.js\n"
printf ".. browse to http://host:80/manage for control\n"
printf ".. output is appended to log.txt\n"
printf ".. press ctrl+c to stop.\n"
+printf "using pm2? commands are listed in script comments.\n"
# start the node process (sri's version)
node nc-multiplex >> log.txt 2>&1
\ No newline at end of file
From 742d40febf22bc4a06070b980a269c8e2ad04e07 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 14:20:26 -0400
Subject: [PATCH 39/47] timestamps: add additional hardening for issue seen on
digital ocean
req.params.graph is undefined
---
nc-multiplex.js | 13 ++++++++++---
start-nc-multiplex.sh | 2 +-
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 516d25c..71b2913 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -782,13 +782,20 @@ app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
* it will spawn a new process if able to.
*/
async function m_RouterLogic(req) {
+ let port;
+ let path = '';
+
+ // sri debug detect if req.params is undefined
if (req.params === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
+ } else if (req.params.graph === undefined) {
+ console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params.graph is undefined');
+ console.log(PRE, $T(), 'req.ip:', req.ip);
}
- const db = req.params.graph;
- let port;
- let path = '';
+
+ // if req.params.graph is unexpectedly undefined, use an unfindable graph name
+ const db = req.params.graph || '';
// Authenticate to allow spawning
let ALLOW_SPAWN = false;
diff --git a/start-nc-multiplex.sh b/start-nc-multiplex.sh
index eb04630..77bc13e 100755
--- a/start-nc-multiplex.sh
+++ b/start-nc-multiplex.sh
@@ -13,10 +13,10 @@
# pm2 start [app] # to start the process
printf "starting nc-multiplex.js\n"
+printf "for pm2 usage: see script comments for command list\n"
printf ".. browse to http://host:80/manage for control\n"
printf ".. output is appended to log.txt\n"
printf ".. press ctrl+c to stop.\n"
-printf "using pm2? commands are listed in script comments.\n"
# start the node process (sri's version)
node nc-multiplex >> log.txt 2>&1
\ No newline at end of file
From c5b44629745e4465838ad78f4be8311bf31e1077 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 14:31:04 -0400
Subject: [PATCH 40/47] timestamps: correct bug in hardening logic
---
nc-multiplex.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 71b2913..2a40dc9 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -795,7 +795,7 @@ async function m_RouterLogic(req) {
}
// if req.params.graph is unexpectedly undefined, use an unfindable graph name
- const db = req.params.graph || '';
+ const db = req.params ? req.params.graph || '' : '';
// Authenticate to allow spawning
let ALLOW_SPAWN = false;
From ca3e675741edcf5beebe63fc7f39a443b397a80f Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 14:51:11 -0400
Subject: [PATCH 41/47] timestamps: comprehensive req checking to
m_RouterGraph(), add additional location.reload() check
---
nc-multiplex.js | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 2a40dc9..6d8295b 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -326,6 +326,7 @@ function RenderManager() {
let unit = 's';
let out = '(' + remaining.toFixed(0) + unit + ' until auto logout)';
if (remaining < 30) status.style.color = 'red';
+ if (remaining < 0) location.reload();
status.innerHTML = out;
}, 1000);
`;
@@ -784,19 +785,24 @@ app.get(`/graph/:graph/${NC_URL_CONFIG}`, (req, res) => {
async function m_RouterLogic(req) {
let port;
let path = '';
+ let db = '';
// sri debug detect if req.params is undefined
- if (req.params === undefined) {
+ if (req === undefined) {
+ console.log(PRE, $T(), 'ERROR in m_RouterLogic: req is undefined');
+ db = '';
+ } else if (req.params === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
+ db = '';
} else if (req.params.graph === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params.graph is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
+ db = '';
+ } else {
+ db = req.params.graph;
}
- // if req.params.graph is unexpectedly undefined, use an unfindable graph name
- const db = req.params ? req.params.graph || '' : '';
-
// Authenticate to allow spawning
let ALLOW_SPAWN = false;
if (CookieIsValid(req)) {
From 32dc9a72b08ab59ceedd6afb9eac0d4672bd569e Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 15:15:19 -0400
Subject: [PATCH 42/47] timestamps: harden autologout of management page where
cookie is not refreshed
---
nc-multiplex.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 6d8295b..975db74 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -318,7 +318,7 @@ function RenderManager() {
const startTime = new Date().getTime();
setInterval( ()=> {
if (!document.cookie.includes('nc-multiplex-auth')) {
- location.reload();
+ location.href='/login?expired';
};
const status = document.getElementById('status');
let elapsed = (new Date().getTime() - startTime) / 1000;
@@ -326,7 +326,8 @@ function RenderManager() {
let unit = 's';
let out = '(' + remaining.toFixed(0) + unit + ' until auto logout)';
if (remaining < 30) status.style.color = 'red';
- if (remaining < 0) location.reload();
+ if (remaining < 1) location.href='/login?expired';
+
status.innerHTML = out;
}, 1000);
`;
From d04fb31e516e2659c8a8682c02868e6d4f508859 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Sun, 25 Aug 2024 15:22:18 -0400
Subject: [PATCH 43/47] timestamps: fix autologout logic to wait longer before
href redirect
---
nc-multiplex.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 975db74..90fc601 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -326,9 +326,8 @@ function RenderManager() {
let unit = 's';
let out = '(' + remaining.toFixed(0) + unit + ' until auto logout)';
if (remaining < 30) status.style.color = 'red';
- if (remaining < 1) location.href='/login?expired';
-
- status.innerHTML = out;
+ if (remaining < 0) location.href='/login?expired';
+ if (remaining >= 0) status.innerHTML = out;
}, 1000);
`;
response += `
` + RenderLoginForm() + `
`;
From 771795a84d09019e7f1f923faa75c26923605147 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Thu, 5 Sep 2024 06:59:50 -0400
Subject: [PATCH 44/47] timestamps: remove more log, sesame patterns
---
.gitignore | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index cda4bd4..ca28dc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,11 +8,14 @@
# ignore custom password file
# this should never be commited to the repo to expose the password
-SESAME
+SESAME*
+
# ignore digital ocean start script, logs
do-start.sh
-log.txt
+log*
+/*.txt
+/_archives
# ignore process saving files
.nc-process-state.json
From e90ae0df29c5efce117e7e233964a6c694aaa675 Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Thu, 5 Sep 2024 07:06:49 -0400
Subject: [PATCH 45/47] timestamps: harden against missing req.originalUrl, if
'err' don't route a path in m_RouterGraph
---
nc-multiplex.js | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index 90fc601..d09d1d1 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -725,7 +725,7 @@ try {
/// If 'SESAME' file exists, use the password in there instead of default
try {
let sesame = fs.readFileSync('SESAME', 'utf8');
- PASSWORD = sesame;
+ PASSWORD = sesame.trim();
} catch (err) {
PASSWORD = DEFAULT_PASSWORD; // no password, use default
}
@@ -786,19 +786,20 @@ async function m_RouterLogic(req) {
let port;
let path = '';
let db = '';
+ let err='';
// sri debug detect if req.params is undefined
if (req === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req is undefined');
- db = '';
+ err = '';
} else if (req.params === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
- db = '';
+ err = '';
} else if (req.params.graph === undefined) {
console.log(PRE, $T(), 'ERROR in m_RouterLogic: req.params.graph is undefined');
console.log(PRE, $T(), 'req.ip:', req.ip);
- db = '';
+ err = '';
} else {
db = req.params.graph;
}
@@ -841,6 +842,10 @@ async function m_RouterLogic(req) {
);
path = `/error_no_database?graph=${db}`;
}
+ if (err) {
+ console.log(PRE, $T(), '!!! m_RouterLogic error:', err);
+ path = undefined;
+ }
return {
protocol: 'http:',
host: 'localhost',
@@ -882,6 +887,11 @@ function m_ProxyFilter(rpath, req) {
// pass if there is a file
// (srinote: this param only contains the first segment, which may be a bug)
if (req.params.file) return true; // only first segment of path (bug?)
+ // check for missing originalUrl
+ if (req.originalUrl===undefined) {
+ console.log(PRE, $T(), '??? ProxyFilter req.originalUrl is undefined');
+ return false;
+ }
// pass if there is a trailing '/'
if (req.params.graph && req.originalUrl.endsWith('/')) return true; // legit graph
return false;
@@ -895,11 +905,11 @@ function m_ProxyRewrite(rpath, req) {
// remove '/graph/db/' for the rerouted calls
// e.g. localhost/graph/hawaii/#/edit/mop => localhost:3000/#/edit/mop
- const fullPath = req.originalUrl;
if (req.originalUrl === undefined) {
console.log(PRE, $T(), '??? ProxyRewrite req.originalUrl is undefined');
return rpath;
}
+ const fullPath = req.originalUrl;
/*/ srinote: in hpm 3, path is the remainder after the /graph/db/ prefix
instead of the full path as before, so use req.originalUrl instead
From ca8ea0af92e6d187d7fc517394532635ea4f4fbe Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Thu, 5 Sep 2024 07:07:09 -0400
Subject: [PATCH 46/47] timestamps: update Readme
---
ReadMe.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ReadMe.md b/ReadMe.md
index beb3ad6..19b6c7e 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -95,7 +95,7 @@ If no `home.html` page is found, the app will display a Net.Create logo and cont
#### 5. Set your Password
By default, the password is `kpop`. We **strongly recommend** you set a custom password.
-To set a new password, create a text file named `SESAME` containing just your password text (no line feed), and place it in the root `/nc-multiplex` folder. Make sure you don't inadvertently insert a **newline** at the end of the file.
+To set a new password, create a text file named `SESAME` containing just your password text (no line feed), and place it in the root `/nc-multiplex` folder.
Or you can:
1. `ssh` to your machine
@@ -106,7 +106,7 @@ Or you can:
#### 6. Start Reverse Proxy Server
```
cd ~/your-dev-folder/nc-multiplex
-./start.sh
+./start-nc-multiplex.sh
```
***IP Address or Google Analytics Code**
From f9973bca6532429e15a5bc48b3a866835979a40b Mon Sep 17 00:00:00 2001
From: DSri Seah <11952933+dsriseah@users.noreply.github.com>
Date: Mon, 9 Sep 2024 15:43:40 -0400
Subject: [PATCH 47/47] timestamps: fix incorrect memory bytes-to-mb conversion
---
nc-multiplex.js | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/nc-multiplex.js b/nc-multiplex.js
index d09d1d1..1a3cc5b 100644
--- a/nc-multiplex.js
+++ b/nc-multiplex.js
@@ -476,7 +476,7 @@ function RenderMemoryReport() {
response += `