diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | package-lock.json | 417 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/scraping/buxton/final/json/buxton.json | 130 | ||||
-rw-r--r-- | src/server/DashSession/DashSessionAgent.ts | 5 | ||||
-rw-r--r-- | src/server/DashSession/Session/agents/applied_session_agent.ts | 58 | ||||
-rw-r--r-- | src/server/DashSession/Session/agents/monitor.ts | 298 | ||||
-rw-r--r-- | src/server/DashSession/Session/agents/process_message_router.ts | 41 | ||||
-rw-r--r-- | src/server/DashSession/Session/agents/promisified_ipc_manager.ts | 173 | ||||
-rw-r--r-- | src/server/DashSession/Session/agents/server_worker.ts | 160 | ||||
-rw-r--r-- | src/server/DashSession/Session/utilities/repl.ts | 128 | ||||
-rw-r--r-- | src/server/DashSession/Session/utilities/session_config.ts | 129 | ||||
-rw-r--r-- | src/server/DashSession/Session/utilities/utilities.ts | 37 | ||||
-rw-r--r-- | src/server/index.ts | 2 |
14 files changed, 1138 insertions, 442 deletions
diff --git a/.gitignore b/.gitignore index 23bd3e0fc..7a5b2ec84 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ solr-8.3.1/server/logs/ solr-8.3.1/server/solr/dash/data/tlog/* solr-8.3.1/server/solr/dash/data/index/* src/scraping/buxton/final/source/ +src/scraping/buxton/final/json/ src/scraping/buxton/source/ src/server/public/files/ src/scraping/acm/package-lock.json diff --git a/package-lock.json b/package-lock.json index 0b8bd140c..f38f23acb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,7 +374,8 @@ "@types/chai": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", - "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==" + "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==", + "dev": true }, "@types/classnames": { "version": "2.2.9", @@ -602,7 +603,8 @@ "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true }, "@types/mongodb": { "version": "3.3.14", @@ -1995,7 +1997,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -5417,8 +5419,8 @@ "bundled": true, "optional": true, "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, "balanced-match": { @@ -5501,14 +5503,14 @@ "bundled": true, "optional": true, "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, "glob": { @@ -5516,12 +5518,12 @@ "bundled": true, "optional": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-unicode": { @@ -5550,8 +5552,8 @@ "bundled": true, "optional": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -5683,10 +5685,10 @@ "bundled": true, "optional": true, "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "number-is-nan": { @@ -5704,7 +5706,7 @@ "bundled": true, "optional": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "os-homedir": { @@ -5741,10 +5743,10 @@ "bundled": true, "optional": true, "requires": { - "deep-extend": "0.6.0", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, "dependencies": { "minimist": { @@ -5759,13 +5761,13 @@ "bundled": true, "optional": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.4", - "isarray": "1.0.0", - "process-nextick-args": "2.0.1", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { @@ -5811,9 +5813,9 @@ "bundled": true, "optional": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { @@ -5861,7 +5863,7 @@ "bundled": true, "optional": true, "requires": { - "string-width": "1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -12629,11 +12631,6 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==" - }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -14249,334 +14246,6 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, - "resilient-server-session": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/resilient-server-session/-/resilient-server-session-1.1.9.tgz", - "integrity": "sha512-XSVujTyJOQMACllXUvWOSHY4GK4JI6aECjCrQR0UBvd2+hdjM1euffspn2b+7M0fepo+bJ71YrAOA9M34ChBZw==", - "requires": { - "@types/chai": "^4.2.7", - "@types/express": "^4.17.2", - "@types/mocha": "^5.2.7", - "@types/node": "^10.12.30", - "@types/request-promise": "^4.1.42", - "@types/uuid": "^3.4.6", - "chai": "^4.2.0", - "colors": "^1.4.0", - "express": "^4.17.1", - "jsonschema": "^1.2.5", - "mocha": "^7.0.0", - "request": "^2.88.0", - "request-promise": "^4.2.5", - "typescript": "^3.7.4", - "uuid": "^3.3.3" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mocha": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.1.tgz", - "integrity": "sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==", - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", - "yargs-unparser": "1.6.0" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "requires": { - "picomatch": "^2.0.4" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" - } - }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index 250509e80..6f54023d6 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,6 @@ "readline": "^1.3.0", "request": "^2.88.0", "request-promise": "^4.2.5", - "resilient-server-session": "^1.1.9", "rimraf": "^3.0.0", "serializr": "^1.5.4", "sharp": "^0.23.4", diff --git a/src/scraping/buxton/final/json/buxton.json b/src/scraping/buxton/final/json/buxton.json index 2048bf24b..bff6f6d23 100644 --- a/src/scraping/buxton/final/json/buxton.json +++ b/src/scraping/buxton/final/json/buxton.json @@ -26,9 +26,9 @@ "shortDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan controller, which is also in the collection.", "longDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller more affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan/SpaceMouse controller, which is also in the collection. Like the Magellan, this is an isometric rate-control joystick. That is, it rests in a neutral central position, not sending and signal. When a force is applied to it, it emits a signal indicating the direction and strength of that force. This signal can then be mapped to a parameter of a selected object, such as a sphere, and – for example – cause that sphere to rotate for as long as, and as fast as, and in the direction determined by, the duration, force, and direction of the applied force. When released, it springs back to neutral position. Note that the force does not need to be directed along a single DOF. In fact, a core feature of the device is that one can simultaneously and independently apply force that asserts control over more than one DOF, and furthermore, vary those forces dynamically. As an aid to understanding, let me walk through some of the underlying concepts at play here by using a more familiar device: a computer mouse. If you move a mouse in a forward/backward direction, the mouse pointer on the screen moves between the screen’s top and bottom. If you think of the screen as a piece of graph paper, that corresponds to moving along the “Y” axis. That is one degree of freedom. On the other hand, you could move the mouse left and right, which causes the mouse to move between the left and right side of the screen. That would correspond to moving along the graph paper’s “X” axis – a second degree of freedom. Yet, you can also move the mouse diagonally. This is an example of independently controlling two degrees of freedom. Now imagine that if you lifted your mouse off your desktop, that your computer could dynamically sense its height as you did so. This would constitute a “flying mouse” (the literal translation of the German word for a “Bat”, which Canadian colleague, Colin Ware, applied to just such a mouse which he built in 1988). If you moved your Bat vertically up and down, perpendicular to the desktop, you would be controlling movement along the “Z” axis - a third degree of freedom. Having already seen that we can move a mouse diagonally, we have established that we need not be constrained to only moving along a single axis. That extends to the movement of our Bat and movement along the “Z” axis. We can control our hand movement in dependently in any or all directions in 3D space. But how does one reconcile the fact that we call the CadMan a “3D controller, and yet also describe it as having 6 degrees of freedom? Yes, as described, our bat can fly in 3D, but on the other hand, its range of movement within those 3 dimensions is much richer. To demonstrate this, move your hand in 3D space on and above your desktop. However, do so keeping your palm flat, parallel to the desktop with your fingers pointing directly forward. In so doing, you are still moving in 3D. Now, while moving, twist your wrist, while moving the hand, such that your palm is alternatively exposed to the left and right side. This constitutes rotation around the “Y” axis. A fourth DOF. Now add a waving motion to your hand, as if it were a paper airplane diving up and down, while also rocking left and right. But keep your fingers pointing forward. You have now added a fifth DOF, rotation around the “X” axis. Finally, add a twist to your wrist so that your fingers are no longer constrained to pointing forward. This is the sixth degree of freedom, rotation around the “Z” axis. Now don’t be fooled, this exercise could continue. We are not restricted to even six DOF. Imagine doing the above, but where the movement and rotations are measured relative to the Bat’s position and orientation, rather than to the holding/controlling hand, per se. One could imagine the Bat having a scroll wheel, like the one on most mice today. Furthermore, while flying your Bat around in 3D, that wheel could easily be rolled in either forward or backward, and thereby control the size of whatever was being controlled. Hence, with one hand we could assert simultaneous and independent control over 7 DOF in 3D space. This exercise has two intended take-aways. The first is a better working understanding between the notion of Degree of Freedom (DOF) and Dimension in space. Hopefully, the confusion frequently encountered when 3D and 6DOF are used in close context, can now be eliminated. Second, is that, with appropriate sensing, the human hand is capable of exercising control over far more degrees of freedom that six. And if we use the two hands together, the potential number of DOF that one can control goes even further. Finally, it is important to add one more take-away – one which both emerges from, and is frequently encountered when discussing, the previous two. That is, do not equate exercising simultaneous control over a high number of DOF with consciously doing the same number of different things all at once. The example that used to be thrown at me when I started talking about coordinated simultaneously bi-manual action went along the lines of, “Psychology tells us that we cannot do multiple things at once, for example, simultaneously tapping your head and rubbing your stomach. ”Well, first, I can tap my head with one hand while rubbing my stomach with the other. But that is not the point. The whole essence of skill – motor-sensory and cognitive – is “chunking” or task integration. When one appears to be doing many different things at once, if they are skilled, they are consciously doing only one thing. Playing a chord on the piano, for example, or skiing down the hill. Likewise, in flying your imaginary BAT in the previous exercise with the scroll wheel, were you doing 7 things at once, or one thing with 7 DOF? And if you had a Bat in each hand, does that mean you are now doing 14 things at once, or are you doing one thing with 14 DOF? Let me provide a different way of answering this question: if you have ever played air guitar, or “conducted” the orchestra that you are listening to on the radio, you are exercising control over more than 14 DOF. And you are doing exactly what I just said, “playing air guitar” or “conducting an orchestra”. One thing – at the conscious level, which is what matters – despite almost any one thing being able to be deconstructed into hundreds of sub-tasks. As I said the essence of skill: aggregation, or chunking. What is most important for both tool designers and users to be mindful of, is the overwhelming influence that our choice and design of tools impacts the degree to which such integration or chunking can take place. The degree to which the tool matches both the skills that we have already acquired through a lifetime of living in the everyday world, and the demands of the intended task, the more seamless that task can be performed, the more “natural” it will feel, and the less learning will be required. In my experience, it brought particular value when used bimanually, in combination with a mouse, where the preferred hand performed conventional pointing, selection and dragging tasks, while the non-preferred hand could manipulate the parameters of the thing being selected. First variation of the since the 2001 formation of 3Dconnextion. The CadMan came in 5 colours: smoke, orange, red, blue and green. See the notes for the LogiCad3D Magellan for more details on this class of device. It is the “parent” of the CadMan, and despite the change in company name, it comes from the same team.", "__images": [ - "http://localhost:1050/files/images/buxton/upload_665169c3-580b-42be-83e1-96024439fa37.png", - "http://localhost:1050/files/images/buxton/upload_95580577-a8bc-4b2f-b787-1132860e726e.png", - "http://localhost:1050/files/images/buxton/upload_2561220d-575f-4070-8fe3-b30fc2d01ead.png" + "http://localhost:1050/files/images/buxton/upload_ec24162e-d82c-4666-b008-28f8562c7701.png", + "http://localhost:1050/files/images/buxton/upload_d7771857-cfce-4b2b-9c12-83ab21592418.png", + "http://localhost:1050/files/images/buxton/upload_e9ceef1a-e48c-40d4-9840-54d8df776210.png" ], "captions": [ "The 3Dconnexion CadMan 3D Motion Controller, a 6DOF joystick.", @@ -71,13 +71,13 @@ "shortDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market.", "longDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market. As illustrated in an accompanying image, the direction of the force which controls each of the 6 degrees of freedom of the SpaceNavigator are: Move Left-Right: Push/Pull left-right parallel to the desktop. Move Forward-Backward: Push/Pull forward-backward parallel to the desktop. Move Vertically, Up-Down: Push down vertically into the table or pull up vertically away from the tableTilt Left-Right: Tilt the joystick left-rightTilt Forward-Backward: Tilt joystick forward-backwardRotate around vertical axis: Twist the joystick clockwise or counter clockwise. Control of these 6 DOF can be combined. For example, you can rotate/roll right while spinning. Besides gaming, one of the hopes was that this device would be used in interacting with 3D programs like Google Earth. The problem was, however, that there were few such programs then, just as now, relatively speaking, and even Google Earth, while remarkable, is not used anywhere as frequently of 2D Google Maps, for example. For those of us in the 3D graphics market, it was fantastic with respect to animation, games and industrial design. But it never took off, no matter how seductive it was. And, the interesting question is, will VR and AR change that? And if so, how will this class of 6DOF device play in that market?", "__images": [ - "http://localhost:1050/files/images/buxton/upload_62108b4e-aa02-44ee-8b2c-562237ece695.jpg", - "http://localhost:1050/files/images/buxton/upload_1d819dcc-458c-4e74-b2fc-5a04ff0965e6.png", - "http://localhost:1050/files/images/buxton/upload_569136d3-3358-444a-85b1-27f15a5ac2cc.png", - "http://localhost:1050/files/images/buxton/upload_97501ca2-9e68-434a-905e-c6a2995f066b.jpg", - "http://localhost:1050/files/images/buxton/upload_7bd955a9-df0a-4f6a-bcb8-bad1a0080c8b.png", - "http://localhost:1050/files/images/buxton/upload_7a15248a-5e9c-4e98-98de-3d918fe2013c.png", - "http://localhost:1050/files/images/buxton/upload_df219f3c-0fc1-4018-aa56-4390532d5edb.png" + "http://localhost:1050/files/images/buxton/upload_d5cd2291-bf01-4b4d-bae1-05c3e241f388.jpg", + "http://localhost:1050/files/images/buxton/upload_8a57b837-9639-4108-bc8e-4c72137285e7.png", + "http://localhost:1050/files/images/buxton/upload_373e64c9-37a5-4662-90af-7ea4bcf09f12.png", + "http://localhost:1050/files/images/buxton/upload_05bde0ce-5523-4f6e-b183-dc06ffcbff5c.jpg", + "http://localhost:1050/files/images/buxton/upload_fb0d908e-fc85-45e8-b237-16b03f3f2fd9.png", + "http://localhost:1050/files/images/buxton/upload_1c23157c-a373-45a8-bae3-1b8bff3f2774.png", + "http://localhost:1050/files/images/buxton/upload_aa4e4bf3-84c1-4a08-a1fc-2eed2ffdb554.png" ], "captions": [ "The 3Dconnexion SpaceNavigator 6DOF Joystick.", @@ -124,11 +124,11 @@ "shortDescription": "The Magellan/SpaceMouse Plus is a refinement of the original LogiCad3D Magellan. From the industrial design perspective, the main difference is the switch from the original round “hockey puck” shaped handle to this asymmetric one.", "longDescription": "The Magellan Plus (also known as the Spacemouse Plus) is a refinement of the original LogiCad3D Magellan (LogiCad3D evolved into 3DConnexion). From the industrial design perspective, the main difference between the two is the switch from the round “hockey puck” shaped handle to this asymmetric one. Despite this rather small change, both are included in the collection since that small change is a good example of my axiom that “everything being best for something and worst for something else. ” One of the things which most attracted me to the original Magellan was its hockey-puck shaped handle. The reason is that in my mind, it shouted out, “jog-shuttle wheel”. That is, the kind of controller that I was familiar with from editing audio and video. Since we were building software for 3D animation, the shape had value as a physical icon, or “phycon”, whose affordances suggested how this new control could employ existing skills. That is the good side. On the other hand, for 3D manipulation, when gripped, that same symmetry lacked any immediate tactile feed-forward as to orientation. That is, what axis of the 3D model would be affected by tilting the handle in any direction. On the other hand, the new asymmetric handle told the user, through touch, how the orientation of the handle aligned with that of the 3D model being controlled. One of the key habits leading to design literacy is to constantly prospect for patterns, rather than just individual examples. The reg the pattern means that one can separate the superficial features of the example, and see the underlying issue. For example, compare the Magellan Plus and the LogiCad3D Magellan, respectfully, with any Apple mouse and the 1988 Apple iMac G3 “hockey puck” mouse. While mice and 3D joysticks are very different devices, the two pairings reflect the same pattern. The suggested lesson is that patterns suggest that certain things are not mere exceptions – they are something which will likely reoccur, and therefore something that one can learn from – as long as one can recognize the deep pattern hidden beneath the superficial exterior.", "__images": [ - "http://localhost:1050/files/images/buxton/upload_30f61adc-4d96-4f0e-a25d-2483c270f608.jpg", - "http://localhost:1050/files/images/buxton/upload_1d9b5679-9acf-47f6-90a9-88a23ca2ba5a.png", - "http://localhost:1050/files/images/buxton/upload_a92f1c7c-fc12-4398-a598-3cab2ef553b9.png", - "http://localhost:1050/files/images/buxton/upload_7a7413c7-8425-4a4b-b5c5-17d33a75077e.png", - "http://localhost:1050/files/images/buxton/upload_efe56c7c-4b19-4fcd-9714-a0478ba970d2.png" + "http://localhost:1050/files/images/buxton/upload_d260b34c-bb42-4545-a72a-9b328312bb0c.jpg", + "http://localhost:1050/files/images/buxton/upload_a34eb179-e591-474a-9826-c52012baf44d.png", + "http://localhost:1050/files/images/buxton/upload_1df8b9fa-8e98-4f15-915f-ba91fde5edea.png", + "http://localhost:1050/files/images/buxton/upload_878b499c-cf6a-44d9-9524-4c287a761174.png", + "http://localhost:1050/files/images/buxton/upload_33ca9556-4e14-41a5-a0b7-fba328fdb4df.png" ], "captions": [ "Overview of the Magellan Plus.", @@ -171,13 +171,13 @@ "shortDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00.", "longDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00. This version of the SpaceBall illustrates how the form-factor of the original version has changed over time. There are now 12 programmable function keys, 9 to be operated by the fingers on one side, and 3 to be operated by the thumb on the other. Note how the button placement indicates that the device is intended to be used by the left hand, with an accompanying mouse by the right – that is, by being meant for the left hand, it is intended for a right handed person.", "__images": [ - "http://localhost:1050/files/images/buxton/upload_a449576a-798f-42a5-9bea-2bda2c617413.jpg", - "http://localhost:1050/files/images/buxton/upload_f753b1f9-2e89-4e62-bbdd-c0f96da3e412.png", - "http://localhost:1050/files/images/buxton/upload_fae1743b-6c9e-43ba-a8fe-a4149bdf02aa.png", - "http://localhost:1050/files/images/buxton/upload_c12978cc-fd4f-463b-b92a-f4e9aadaf51d.png", - "http://localhost:1050/files/images/buxton/upload_284fe4cb-d873-407a-93be-6816f4401796.jpg", - "http://localhost:1050/files/images/buxton/upload_2383fc94-1b15-406b-9818-eacbdacd9e45.jpg", - "http://localhost:1050/files/images/buxton/upload_70701ee4-6752-4755-a338-55a889e1c20a.jpg" + "http://localhost:1050/files/images/buxton/upload_c07632e5-3716-4041-a721-e13fbeccf0db.jpg", + "http://localhost:1050/files/images/buxton/upload_9ea9e885-5acc-4fe2-8a31-8365685281fe.png", + "http://localhost:1050/files/images/buxton/upload_f9c85b72-2aa6-4b1e-ab53-05d5120c3d98.png", + "http://localhost:1050/files/images/buxton/upload_b8759cab-029f-4e19-bd80-5346d4822eaa.png", + "http://localhost:1050/files/images/buxton/upload_91edf9a2-b929-4c15-b122-f9ebc1a78c62.jpg", + "http://localhost:1050/files/images/buxton/upload_3fb9fb0c-e7e8-454a-9942-3993d5be9db9.jpg", + "http://localhost:1050/files/images/buxton/upload_d672132a-598f-4a14-ad79-40169c3a0cbf.jpg" ], "captions": [ "Side view of the Spaceball 5000.", @@ -218,12 +218,12 @@ "shortDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The compromise of this, however, is that one loses some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm.", "longDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The joystick also came in two sizes so as to better accommodate different hand sizes. There almost always trade-offs in design. Often more than one. In this case, one compromise is a loss of some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm. Another is the increased time/attention required when moving from the keyboard to the mouse. To do a simple test, move your hand to a conventional mouse. Now place a water glass which is taller than it is wide in the same position as the mouse, and compare how fast you can get it “in hand” enough to control its movement as well as you could the mouse. Try the same thing while not looking – using motor memory. Then consider how often you make that change. And, by the same token, consider how many different ways you can hold your mouse while still using it, versus a joystick shaped mouse with the button on top. In none of this am I complaining about, nor criticizing this mouse design. Rather, I am trying to illustrate that there is a lot to consider, and as either a designer or consumer, these are things to train oneself to notice and question. From such experience emerges the basis for better choices in both design and purchase. .", "__images": [ - "http://localhost:1050/files/images/buxton/upload_84007f47-f842-46e9-9e99-e45f5ecc72ee.jpg", - "http://localhost:1050/files/images/buxton/upload_e1f1da29-4254-446c-85e8-c578ab770936.jpg", - "http://localhost:1050/files/images/buxton/upload_67547a81-89bc-4d38-a9d4-1a17510f0a86.jpg", - "http://localhost:1050/files/images/buxton/upload_0b94009a-251b-4c77-ae0a-c1df7a227d64.jpg", - "http://localhost:1050/files/images/buxton/upload_f591252d-7e21-4389-ab2b-dbc60d1db43e.jpg", - "http://localhost:1050/files/images/buxton/upload_60df3ea2-532b-4d3d-8f47-7ca9ddf30533.jpg" + "http://localhost:1050/files/images/buxton/upload_dff0fd55-9e6f-41d0-9587-b319924d23bf.jpg", + "http://localhost:1050/files/images/buxton/upload_db81c490-daba-4cc2-9120-e195dd655216.jpg", + "http://localhost:1050/files/images/buxton/upload_55589947-fd88-414b-9199-4ccc75e8bbf7.jpg", + "http://localhost:1050/files/images/buxton/upload_f0ae2f45-066f-4149-8f8e-31ba9135241c.jpg", + "http://localhost:1050/files/images/buxton/upload_b9e86b1e-6b52-4d66-b4f5-c83c72ecdc43.jpg", + "http://localhost:1050/files/images/buxton/upload_e684af15-6a20-4970-9f97-8d50d5c299bb.jpg" ], "captions": [ "A view of the 3M Ergonomic Mouse in the grasp of the right hand, with the thumb activating the button at the top of the stem.", @@ -260,13 +260,13 @@ "shortDescription": "A relatively early trackball for the Apple Macintosh computer. Note the positioning of the buttons, which bias the device for right-handed use, and using the thumb for the buttons and fingers for manipulating the ball.", "longDescription": "A relatively early trackball for the Apple Macintosh computer. The larger button was functionally equivalent to the mouse button, and the smaller one was a ‘lock’ button. The lock button is analogous to the SHIFT LOCK or CAPS LOCK on a QWERTY keyboard: With the keyboard, it means that one does not have to hold the SHIFT key down while typing a string of upper-case characters. With the trackball, it means that you don’t have to hold the primary “mouse” button down while rolling the trackball. The need that this meets can be easily be seen if one compares the relative difficulty of drawing a line by moving a mouse while holding down its button, compared to doing the same task with a trackball. With the mouse, the wrist and forearm mainly move the mouse, and the fingers are used to hold it, as well as the button. With the trackball, the fingers are engaged in rolling the ball as well as holding the button. Hence, the probability of task interference is high, just as with typing a string of upper-case characters on a keyboard without a SHIFT LOCK key. Next, in looking at trackballs, pay attention to the position of the buttons relative to the trackball itself. How this relationship varies across devices and says a lot about how the designers envisioned the device being used. For example, the relationship may indicate the design intends for the thumb or fingers to operate the ball. How does the relationship impact the device’s ability to accommodate left and right hand usage equally well? This latter point is especially important when the trackball is used simultaneously with a mouse. An example would be if the trackball was used to scroll a spreadsheet up-down / left-right, while the mouse was used to point, select, and/or drag. In this case, for example, the trackball would usually be operated by the non-dominant hand and the mouse by the dominant one. Yet, when used alone, the same user would typically operate the trackball with the dominant hand. The lesson from this example is the recognition that handedness is a factor of use (one vs two handed), not just a factor of whether the user’s left or right hand is dominant.", "__images": [ - "http://localhost:1050/files/images/buxton/upload_fa8f6ec4-f909-40db-8b90-fc9ce6f11d8a.jpg", - "http://localhost:1050/files/images/buxton/upload_a5a08c72-5907-4ac2-8894-dcfb3708eaaf.jpg", - "http://localhost:1050/files/images/buxton/upload_87a4bfd6-1dc3-4fd9-bd86-0e04f024207a.jpg", - "http://localhost:1050/files/images/buxton/upload_5e57d61a-3f23-465e-85cb-0f27fa613055.jpg", - "http://localhost:1050/files/images/buxton/upload_c6f4c165-0e97-48c5-a046-719c3a73f7bf.jpg", - "http://localhost:1050/files/images/buxton/upload_9abd3f27-27b6-4ab7-8ff3-a7654a5ffe19.jpg", - "http://localhost:1050/files/images/buxton/upload_1301c7a6-8946-411c-b741-71f36eebbb9d.jpg" + "http://localhost:1050/files/images/buxton/upload_6a262381-2d08-40c2-aa4a-cc1018056164.jpg", + "http://localhost:1050/files/images/buxton/upload_9face080-36e9-4f13-a08c-07614f204b1a.jpg", + "http://localhost:1050/files/images/buxton/upload_f52646b2-4f28-4b4d-96bd-bcbe4f3b7528.jpg", + "http://localhost:1050/files/images/buxton/upload_73e4bc72-a988-4f45-bf6d-09eb78108719.jpg", + "http://localhost:1050/files/images/buxton/upload_586dba0e-808b-45b6-94ad-6a3f9199f1b5.jpg", + "http://localhost:1050/files/images/buxton/upload_cd31d8ed-fd0a-4eda-b5a4-2452cf9f719d.jpg", + "http://localhost:1050/files/images/buxton/upload_1ecf12d1-553f-4a56-98dc-ffa2d9cea3be.jpg" ], "captions": [ "An upper-left view of the Abaton ProPoint trackball.", @@ -307,8 +307,8 @@ "shortDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing.", "longDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing. .", "__images": [ - "http://localhost:1050/files/images/buxton/upload_711a1cdb-c895-426b-9cff-7e1f023d17a5.jpg", - "http://localhost:1050/files/images/buxton/upload_335794e7-c52b-4bdc-b193-b5f653407e4e.png" + "http://localhost:1050/files/images/buxton/upload_11b3c807-910d-42a0-9a46-2c5dce1385d5.jpg", + "http://localhost:1050/files/images/buxton/upload_14b155af-925d-4144-9ad3-e2c0c2f76900.png" ], "captions": [ "View of the Active Book prototype.", @@ -346,11 +346,11 @@ "shortDescription": "The Mini-Touch Keyboard is a small-footprint keyboard with a centrally mounted touchpad. It initially was released with a PS/2 connector, and then in 2006 the connector was updated to USB. While keyboards with integrated touchpads had been available since the mid-1980s, small-footprint ones with centrally mounted touch pads were far less common.", "longDescription": "Released in 2003, this is a small add-on keyboard with an integrated touchpad. While there had been keyboards released with touchpads earlier – see the 1985 KeyTronic LT Touchpad Keyboard in the collection, for example – these were full-sized keyboard, typically with the touchpad mounted at the side, rather than the middle. Keyboards such as the Adesso ACK-540PB, were styled after the smaller foot-print keyboards then becoming standard on laptops, in terms of the central placement of the touchpad, as well as size. This central placement was significant, since it gave equal access to either right or left hand. The touchpad used was a Glidepoint, a 1994 stand-alone version of which is in the collection, the 1994 Cirque Glidepoint. This first model of the ACK-540 was released in both black (ACK-540PB) and white (ACK-540PW) and came with a PS/2 connector. In 2006, black and white versions updated with a USB connectors were released (the ACK-540UB and ACK-540UW) were released – an indication that the product had sustained a place in the market. These same keyboards were also marketed under different brand names, including SolidTek and Daltaco.", "__images": [ - "http://localhost:1050/files/images/buxton/upload_9e20c2c2-8bbe-4f9e-90af-9c547f7dc7f4.jpg", - "http://localhost:1050/files/images/buxton/upload_8848eb38-a7cf-43a4-879d-4440ce475117.jpg", - "http://localhost:1050/files/images/buxton/upload_21b95d38-7a73-446b-be2f-18c4981aed18.jpg", - "http://localhost:1050/files/images/buxton/upload_051c82ac-c2be-4ab7-83d8-91f9619e0bc3.jpg", - "http://localhost:1050/files/images/buxton/upload_fcff645c-2332-4982-a9ed-e918f465fcad.jpg" + "http://localhost:1050/files/images/buxton/upload_9df362f9-3cf4-40a5-952b-f05df4e879c7.jpg", + "http://localhost:1050/files/images/buxton/upload_ced4d71d-5bb4-4ce2-a65e-d3fbdd4c70a2.jpg", + "http://localhost:1050/files/images/buxton/upload_cf374279-502a-470c-b88a-1afba9671ef0.jpg", + "http://localhost:1050/files/images/buxton/upload_e5c5da1a-0b1e-4f0f-ba04-866635a60e4c.jpg", + "http://localhost:1050/files/images/buxton/upload_87b23d06-2c1b-4fb3-8326-c71d86cdce62.jpg" ], "captions": [ "Top view of the Adesso Mini-Touch Keyboard with Touchpad.", @@ -395,17 +395,17 @@ "shortDescription": "This is a mouse / keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided.", "longDescription": "This is a mouse / numerical keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided. There are a few mice with integrated keypads included in the collection. Each takes a different approach in terms of intent as well as industrial design. Comparing them is a worthwhile exercise. By the same token, the approach taken by this example echoes that taken by a very different product, but with the same intent: layer complexity. In this case the president from the collection is a TV/VCR remote control: the 1990 Sony RMT-V5A. For comparison, see the accompanying photo, and for more information, look at the Sony’s detailed device description. Finally, in drawing attention to this specific example rather than one of the hybrid mouse/keypads referred to earlier, the intent is to show that inspiration for design solutions for one class of device can come from those of very different categories. For sure, look at previous in-class solutions. But that just puts you on par with most other designers. The best exercise their creativity by looking in far less explored places. Note to the sharp eyed: If you look at the bottom of the mouse, you will see it marked model KM-1411, while I have been referring to it, as well as the Adesso Web Site, as model AKP-170. Rest assured, these are the same thing, as can be seen on the bottom right corner of the back of the box, where both numbers appear. I too am confused as to why, but also reassured. .", "__images": [ - "http://localhost:1050/files/images/buxton/upload_3bbf35c9-587a-4731-9e0c-f53927703fcc.jpg", - "http://localhost:1050/files/images/buxton/upload_508b066c-1727-4830-b373-a10f55036dff.jpg", - "http://localhost:1050/files/images/buxton/upload_8610309e-ff5c-463b-ac6f-e08661d760b6.jpg", - "http://localhost:1050/files/images/buxton/upload_a3ee650e-3d86-4c3c-8d79-c2c68ba29aa4.jpg", - "http://localhost:1050/files/images/buxton/upload_84a69023-f88a-41ff-94b9-f7a639e1d895.jpg", - "http://localhost:1050/files/images/buxton/upload_3cc21d2b-1702-4095-91f0-a084a264baa0.jpg", - "http://localhost:1050/files/images/buxton/upload_7513ff62-a57e-441e-add3-edaf30ba5037.jpg", - "http://localhost:1050/files/images/buxton/upload_189c54c1-7e83-4d0a-8836-0e608d095b50.jpg", - "http://localhost:1050/files/images/buxton/upload_f50ebb26-8033-422f-99cc-f3a0581921cf.jpg", - "http://localhost:1050/files/images/buxton/upload_3a1d5977-3e1b-459d-bb6e-36b478f1bd15.jpg", - "http://localhost:1050/files/images/buxton/upload_f4414860-f5d5-46f2-881a-6d728050fb79.jpg" + "http://localhost:1050/files/images/buxton/upload_b79208d8-363e-4066-9407-cce53c72df1c.jpg", + "http://localhost:1050/files/images/buxton/upload_a7f22078-9323-410c-982d-59626e330669.jpg", + "http://localhost:1050/files/images/buxton/upload_ddb4c6cc-b53d-44d7-a655-6f2d09da812d.jpg", + "http://localhost:1050/files/images/buxton/upload_d52e453d-9cbd-4c37-9bb9-37c7d97b211e.jpg", + "http://localhost:1050/files/images/buxton/upload_feffbe2b-70ce-4fc3-946b-f164d6cdd696.jpg", + "http://localhost:1050/files/images/buxton/upload_05b2eec5-8725-4720-8695-31eb28f90c6c.jpg", + "http://localhost:1050/files/images/buxton/upload_d97125f6-d943-494e-a5c1-328ea906a2dd.jpg", + "http://localhost:1050/files/images/buxton/upload_38b62fb2-0ab6-4aef-bc26-6b9b3ed0a72b.jpg", + "http://localhost:1050/files/images/buxton/upload_df80bf30-2d5c-438d-ad69-8b11b276fc30.jpg", + "http://localhost:1050/files/images/buxton/upload_2e0f1697-909c-4238-b1b3-7fea24fa42f0.jpg", + "http://localhost:1050/files/images/buxton/upload_1ff53018-dab2-4066-bab5-dced64e4af63.jpg" ], "captions": [ "An overview of the Adesso AKP-170 Mouse with the transparent hinged keypad cover open. Note that the mouse controls can still be accessed.", @@ -461,18 +461,18 @@ "shortDescription": "This is a little-known innovative mouse worth study. It is one of the first to have wireless charging – by sitting on its mouse pad. It has two scroll wheels which are mounted at right angles – like an Etch-a-Sketch, to conform to the direction to be scrolled (up/down vs left/right. It also has a dedicated button which “double clicks” with one push.", "longDescription": "This mouse fell below the radar, and yet it had remarkable innovations in its design, especially given its price. Like any mouse, it provided 2 degrees of freedom for pointing. Unlike most scroll-wheel mouse, it had two separate scroll wheels mounted at right angles. Hence, the orientation of each wheel provided a cue as to which direction the affected document would scroll – up/down or left/right. It is interesting to consider this scroll-wheel layout with that on a companion mouse, the A4 Tech Model IRW-5 4D Wireless Mouse, also in the collection, and shown side-by-side in one of the accompanying photographs. While the scroll wheels of the NF-75 are at right angles, those of the IRW-5 are parallel. Such differences should always provoke the question “Why? ” – for would-be designer and consumer alike. As a memory aid, and the conform to what psychologists call “stimulus-response (S: -R) compatibility”, the right-angle arrangement seems to be far better mapping of action to effect. On the other hand, try an experiment. First, place your hand, palm facing down, on a desktop as if you were holding a mouse. All fingers and thumb should be touching the desktop, but not your palm. Maintaining that position, left your index (scrolling) finger, and repeatedly “fold” and “unfold” it between a pointing posture, and touching your palm. Next, keeping your hand in the same position, this time, again extend your index finger. But this time, move its tip back-and-forth in a left-right motion. What I hope you quickly perceive is that the finger was “optimized” for the folding rather than lateral motion. You “feel” that from the difference in tension in the hand in the two cases of the “study”. The two conditions of our “quick” study roughly mimic the motor actions required to operate the scroll wheels. Since the brief test suggests the possibility that repeated lateral movement may accelerate the onset of repetitive stress injury, it would therefore also suggest that further studies should be undertaken before using a left-right scroll wheel – despite the acknowledged advantage with respect to S: R compatibility. In addition to the select button on virtually all mice, there was an additional button which issued a double-click on a single push. Hence, clicking one button would select, while pushing the other would select and open. While for many, the cost of an extra button may not seem worth saving the effort of a double click, those with some motor impairment may disagree. This is an example where knowing about such things may trigger useful insights to improve future designs. However, perhaps the biggest innovation with this line of A4 mice is that they were powered wirelessly. That is, there were no batteries in the mouse that might die at the wrong moment. Power was provided wirelessly from the mouse pad. Yet, one again we see that design is full of trade-offs. The cost of providing this feature is that of requiring not just the mouse pad, but a special one – something not overly attractive to road-warriors whose brief-cases were already weighted down by cables and other paraphernalia which took up more space than the laptop itself. .", "__images": [ - "http://localhost:1050/files/images/buxton/upload_688cc54e-65b3-41c1-914d-132e8dcc780e.jpg", - "http://localhost:1050/files/images/buxton/upload_ac087ded-408c-48ba-855e-dab3cb43d79f.jpg", - "http://localhost:1050/files/images/buxton/upload_a55b8397-ed11-4566-8ee7-e1c25e507325.jpg", - "http://localhost:1050/files/images/buxton/upload_5f5b280c-2f63-4094-a6ca-37b3b292a55d.jpg", - "http://localhost:1050/files/images/buxton/upload_48929587-218d-48c4-ac89-fceb5fb87992.jpg", - "http://localhost:1050/files/images/buxton/upload_34b3f6d1-8c50-468e-ae88-fcf13e79f1e0.jpg", - "http://localhost:1050/files/images/buxton/upload_185fc393-b4bf-4f0c-836f-4304b6910c8b.jpg", - "http://localhost:1050/files/images/buxton/upload_4eb7d62e-efc7-45ac-99ef-1f474aab0201.jpg", - "http://localhost:1050/files/images/buxton/upload_25313cb4-1be5-4d18-8b06-2bbbcd102aca.jpg", - "http://localhost:1050/files/images/buxton/upload_58474787-7538-4df7-ac60-b27d45a6dd08.jpg", - "http://localhost:1050/files/images/buxton/upload_e088cb02-9ac3-4e7e-84d0-223b5d737966.jpg", - "http://localhost:1050/files/images/buxton/upload_f244be9b-ef53-409d-9b3e-6f4610010cec.jpg" + "http://localhost:1050/files/images/buxton/upload_78940259-52ab-4ac5-94b3-4a32f00eff90.jpg", + "http://localhost:1050/files/images/buxton/upload_de4ce84d-f097-434c-95ae-657296746d02.jpg", + "http://localhost:1050/files/images/buxton/upload_cfdebc2f-833a-4b6b-8595-4885c37b083a.jpg", + "http://localhost:1050/files/images/buxton/upload_762d8358-4d9c-46e4-80d7-e192036228f7.jpg", + "http://localhost:1050/files/images/buxton/upload_4bc15098-7e45-4373-8a09-c45e48a597ae.jpg", + "http://localhost:1050/files/images/buxton/upload_856be213-9c05-4f4d-b867-eaf1c859eb26.jpg", + "http://localhost:1050/files/images/buxton/upload_bb2ef965-3857-47be-a8af-facaf6df25c9.jpg", + "http://localhost:1050/files/images/buxton/upload_507c84e2-d81b-42dc-9b09-ae16363aef9e.jpg", + "http://localhost:1050/files/images/buxton/upload_a367e86b-8cd0-4196-893f-83c1fa80177f.jpg", + "http://localhost:1050/files/images/buxton/upload_27a1b41d-9a3b-4443-97bc-99f9995d88d0.jpg", + "http://localhost:1050/files/images/buxton/upload_3c52528d-56cb-4f86-b6d4-d23e8ea15d85.jpg", + "http://localhost:1050/files/images/buxton/upload_16af92b8-2057-4489-89c4-ad416fbff2f6.jpg" ], "captions": [ "The A4 Tech Model NB-75D optical mouse on its charging mouse pad.", diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index d61e9aac1..85bfe14de 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -8,8 +8,11 @@ import { launchServer, onWindows } from ".."; import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; import * as Archiver from "archiver"; import { resolve } from "path"; -import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; import rimraf = require("rimraf"); +import { AppliedSessionAgent, ExitHandler } from "./Session/agents/applied_session_agent"; +import { ServerWorker } from "./Session/agents/server_worker"; +import { Monitor } from "./Session/agents/monitor"; +import { MessageHandler } from "./Session/agents/promisified_ipc_manager"; /** * If we're the monitor (master) thread, we should launch the monitor logic for the session. diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts new file mode 100644 index 000000000..46c9e22ed --- /dev/null +++ b/src/server/DashSession/Session/agents/applied_session_agent.ts @@ -0,0 +1,58 @@ +import { isMaster } from "cluster"; +import { Monitor } from "./monitor"; +import { ServerWorker } from "./server_worker"; +import { Utilities } from "../utilities/utilities"; + +export type ExitHandler = (reason: Error | boolean) => void | Promise<void>; + +export abstract class AppliedSessionAgent { + + // the following two methods allow the developer to create a custom + // session and use the built in customization options for each thread + protected abstract async initializeMonitor(monitor: Monitor): Promise<string>; + protected abstract async initializeServerWorker(): Promise<ServerWorker>; + + private launched = false; + + public killSession = (reason: string, graceful = true, errorCode = 0) => { + const target = isMaster ? this.sessionMonitor : this.serverWorker; + target.killSession(reason, graceful, errorCode); + } + + private sessionMonitorRef: Monitor | undefined; + public get sessionMonitor(): Monitor { + if (!isMaster) { + this.serverWorker.emit("kill", { + graceful: false, + reason: "Cannot access the session monitor directly from the server worker thread.", + errorCode: 1 + }); + throw new Error(); + } + return this.sessionMonitorRef!; + } + + private serverWorkerRef: ServerWorker | undefined; + public get serverWorker(): ServerWorker { + if (isMaster) { + throw new Error("Cannot access the server worker directly from the session monitor thread"); + } + return this.serverWorkerRef!; + } + + public async launch(): Promise<void> { + if (!this.launched) { + this.launched = true; + if (isMaster) { + this.sessionMonitorRef = Monitor.Create() + const sessionKey = await this.initializeMonitor(this.sessionMonitorRef); + this.sessionMonitorRef.finalize(sessionKey); + } else { + this.serverWorkerRef = await this.initializeServerWorker(); + } + } else { + throw new Error("Cannot launch a session thread more than once per process."); + } + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts new file mode 100644 index 000000000..1d4ea6fb5 --- /dev/null +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -0,0 +1,298 @@ +import { ExitHandler } from "./applied_session_agent"; +import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config"; +import Repl, { ReplAction } from "../utilities/repl"; +import { isWorker, setupMaster, on, Worker, fork } from "cluster"; +import { manage, MessageHandler } from "./promisified_ipc_manager"; +import { red, cyan, white, yellow, blue } from "colors"; +import { exec, ExecOptions } from "child_process"; +import { validate, ValidationError } from "jsonschema"; +import { Utilities } from "../utilities/utilities"; +import { readFileSync } from "fs"; +import IPCMessageReceiver from "./process_message_router"; +import { ServerWorker } from "./server_worker"; + +/** + * Validates and reads the configuration file, accordingly builds a child process factory + * and spawns off an initial process that will respawn as predecessors die. + */ +export class Monitor extends IPCMessageReceiver { + private static count = 0; + private finalized = false; + private exitHandlers: ExitHandler[] = []; + private readonly config: Configuration; + private activeWorker: Worker | undefined; + private key: string | undefined; + private repl: Repl; + + public static Create() { + if (isWorker) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create a monitor on the worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else if (++Monitor.count > 1) { + console.error(red("cannot create more than one monitor.")); + process.exit(1); + } else { + return new Monitor(); + } + } + + private constructor() { + super(); + console.log(this.timestamp(), cyan("initializing session...")); + this.configureInternalHandlers(); + this.config = this.loadAndValidateConfiguration(); + this.initializeClusterFunctions(); + this.repl = this.initializeRepl(); + } + + protected configureInternalHandlers = () => { + // handle exceptions in the master thread - there shouldn't be many of these + // the IPC (inter process communication) channel closed exception can't seem + // to be caught in a try catch, and is inconsequential, so it is ignored + process.on("uncaughtException", ({ message, stack }): void => { + if (message !== "Channel closed") { + this.mainLog(red(message)); + if (stack) { + this.mainLog(`uncaught exception\n${red(stack)}`); + } + } + }); + + this.on("kill", ({ reason, graceful, errorCode }) => this.killSession(reason, graceful, errorCode)); + this.on("lifecycle", ({ event }) => console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${event})`)); + } + + private initializeClusterFunctions = () => { + // determines whether or not we see the compilation / initialization / runtime output of each child server process + const output = this.config.showServerOutput ? "inherit" : "ignore"; + setupMaster({ stdio: ["ignore", output, output, "ipc"] }); + + // a helpful cluster event called on the master thread each time a child process exits + on("exit", ({ process: { pid } }, code, signal) => { + const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; + this.mainLog(cyan(prompt)); + // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one + this.spawn(); + }); + } + + public finalize = (sessionKey: string): void => { + if (this.finalized) { + throw new Error("Session monitor is already finalized"); + } + this.finalized = true; + this.key = sessionKey; + this.spawn(); + } + + public readonly coreHooks = Object.freeze({ + onCrashDetected: (listener: MessageHandler<{ error: Error }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener), + onServerRunning: (listener: MessageHandler<{ isFirstTime: boolean }>) => this.on(Monitor.IntrinsicEvents.ServerRunning, listener) + }); + + /** + * Kill this session and its active child + * server process, either gracefully (may wait + * indefinitely, but at least allows active networking + * requests to complete) or immediately. + */ + public killSession = async (reason: string, graceful = true, errorCode = 0) => { + this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`)); + this.mainLog(`session exit reason: ${(red(reason))}`); + await this.executeExitHandlers(true); + await this.killActiveWorker(graceful, true); + process.exit(errorCode); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Extend the default repl by adding in custom commands + * that can invoke application logic external to this module + */ + public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + this.repl.registerCommand(basename, argPatterns, action); + } + + public exec = (command: string, options?: ExecOptions) => { + return new Promise<void>(resolve => { + exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => { + if (error) { + this.execLog(red(`unable to execute ${white(command)}`)); + error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`))); + } else { + let outLines: string[], errorLines: string[]; + if ((outLines = stdout.split("\n").filter(line => line.length)).length) { + outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`))); + } + if ((errorLines = stderr.split("\n").filter(line => line.length)).length) { + errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`))); + } + } + resolve(); + }); + }); + } + + /** + * Generates a blue UTC string associated with the time + * of invocation. + */ + private timestamp = () => blue(`[${new Date().toUTCString()}]`); + + /** + * A formatted, identified and timestamped log in color + */ + public mainLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams); + } + + /** + * A formatted, identified and timestamped log in color for non- + */ + private execLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams); + } + + /** + * Reads in configuration .json file only once, in the master thread + * and pass down any variables the pertinent to the child processes as environment variables. + */ + private loadAndValidateConfiguration = (): Configuration => { + let config: Configuration; + try { + console.log(this.timestamp(), cyan("validating configuration...")); + config = JSON.parse(readFileSync('./session.config.json', 'utf8')); + const options = { + throwError: true, + allowUnknownAttributes: false + }; + // ensure all necessary and no excess information is specified by the configuration file + validate(config, configurationSchema, options); + config = Utilities.preciseAssign({}, defaultConfig, config); + } catch (error) { + if (error instanceof ValidationError) { + console.log(red("\nSession configuration failed.")); + console.log("The given session.config.json configuration file is invalid."); + console.log(`${error.instance}: ${error.stack}`); + process.exit(0); + } else if (error.code === "ENOENT" && error.path === "./session.config.json") { + console.log(cyan("Loading default session parameters...")); + console.log("Consider including a session.config.json configuration file in your project root for customization."); + config = Utilities.preciseAssign({}, defaultConfig); + } else { + console.log(red("\nSession configuration failed.")); + console.log("The following unknown error occurred during configuration."); + console.log(error.stack); + process.exit(0); + } + } finally { + const { identifiers } = config!; + Object.keys(identifiers).forEach(key => { + const resolved = key as keyof Identifiers; + const { text, color } = identifiers[resolved]; + identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`); + }); + return config!; + } + } + + /** + * Builds the repl that allows the following commands to be typed into stdin of the master thread. + */ + private initializeRepl = (): Repl => { + const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` }); + const boolean = /true|false/; + const number = /\d+/; + const letters = /[a-zA-Z]+/; + repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); + repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean")); + repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true")); + repl.registerCommand("set", [/polling/, number, boolean], args => { + const newPollingIntervalSeconds = Math.floor(Number(args[1])); + if (newPollingIntervalSeconds < 0) { + this.mainLog(red("the polling interval must be a non-negative integer")); + } else { + if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { + this.config.polling.intervalSeconds = newPollingIntervalSeconds; + if (args[2] === "true") { + Monitor.IPCManager.emit("updatePollingInterval", { newPollingIntervalSeconds }); + } + } + } + }); + return repl; + } + + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Attempts to kill the active worker gracefully, unless otherwise specified. + */ + private killActiveWorker = async (graceful = true, isSessionEnd = false): Promise<void> => { + if (this.activeWorker && !this.activeWorker.isDead()) { + if (graceful) { + Monitor.IPCManager.emit("manualExit", { isSessionEnd }); + } else { + await ServerWorker.IPCManager.destroy(); + this.activeWorker.process.kill(); + } + } + } + + /** + * Allows the caller to set the port at which the target (be it the server, + * the websocket, some other custom port) is listening. If an immediate restart + * is specified, this monitor will kill the active child and re-launch the server + * at the port. Otherwise, the updated port won't be used until / unless the child + * dies on its own and triggers a restart. + */ + private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => { + if (value > 1023 && value < 65536) { + this.config.ports[port] = value; + if (immediateRestart) { + this.killActiveWorker(); + } + } else { + this.mainLog(red(`${port} is an invalid port number`)); + } + } + + /** + * Kills the current active worker and proceeds to spawn a new worker, + * feeding in configuration information as environment variables. + */ + private spawn = async (): Promise<void> => { + await this.killActiveWorker(); + const { config: { polling, ports }, key } = this; + this.activeWorker = fork({ + pollingRoute: polling.route, + pollingFailureTolerance: polling.failureTolerance, + serverPort: ports.server, + socketPort: ports.socket, + pollingIntervalSeconds: polling.intervalSeconds, + session_key: key + }); + Monitor.IPCManager = manage(this.activeWorker.process, this.handlers); + this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`)); + } + +} + +export namespace Monitor { + + export enum IntrinsicEvents { + KeyGenerated = "key_generated", + CrashDetected = "crash_detected", + ServerRunning = "server_running" + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts new file mode 100644 index 000000000..6cc8aa941 --- /dev/null +++ b/src/server/DashSession/Session/agents/process_message_router.ts @@ -0,0 +1,41 @@ +import { MessageHandler, PromisifiedIPCManager, HandlerMap } from "./promisified_ipc_manager"; + +export default abstract class IPCMessageReceiver { + + protected static IPCManager: PromisifiedIPCManager; + protected handlers: HandlerMap = {}; + + protected abstract configureInternalHandlers: () => void; + + /** + * Add a listener at this message. When the monitor process + * receives a message, it will invoke all registered functions. + */ + public on = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (!handlers) { + this.handlers[name] = [handler]; + } else { + handlers.push(handler); + } + } + + /** + * Unregister a given listener at this message. + */ + public off = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + /** + * Unregister all listeners at this message. + */ + public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]); + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts new file mode 100644 index 000000000..9f0db8330 --- /dev/null +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -0,0 +1,173 @@ +import { Utilities } from "../utilities/utilities"; +import { ChildProcess } from "child_process"; + +/** + * Convenience constructor + * @param target the process / worker to which to attach the specialized listeners + */ +export function manage(target: IPCTarget, handlers?: HandlerMap) { + return new PromisifiedIPCManager(target, handlers); +} + +/** + * Captures the logic to execute upon receiving a message + * of a certain name. + */ +export type HandlerMap = { [name: string]: MessageHandler[] }; + +/** + * This will always literally be a child process. But, though setting + * up a manager in the parent will indeed see the target as the ChildProcess, + * setting up a manager in the child will just see itself as a regular NodeJS.Process. + */ +export type IPCTarget = NodeJS.Process | ChildProcess; + +/** + * Specifies a general message format for this API + */ +export type Message<T = any> = { + name: string; + args?: T; +}; +export type MessageHandler<T = any> = (args: T) => (any | Promise<any>); + +/** + * When a message is emitted, it is embedded with private metadata + * to facilitate the resolution of promises, etc. + */ +interface InternalMessage extends Message { metadata: Metadata } +interface Metadata { isResponse: boolean; id: string } +type InternalMessageHandler = (message: InternalMessage) => (any | Promise<any>); + +/** + * Allows for the transmission of the error's key features over IPC. + */ +export interface ErrorLike { + name?: string; + message?: string; + stack?: string; +} + +/** + * The arguments returned in a message sent from the target upon completion. + */ +export interface Response<T = any> { + results?: T[]; + error?: ErrorLike; +} + +const destroyEvent = "__destroy__"; + +/** + * This is a wrapper utility class that allows the caller process + * to emit an event and return a promise that resolves when it and all + * other processes listening to its emission of this event have completed. + */ +export class PromisifiedIPCManager { + private readonly target: IPCTarget; + private pendingMessages: { [id: string]: string } = {}; + private isDestroyed = false; + private get callerIsTarget() { + return process.pid === this.target.pid; + } + + constructor(target: IPCTarget, handlers?: HandlerMap) { + this.target = target; + if (handlers) { + handlers[destroyEvent] = [this.destroyHelper]; + this.target.addListener("message", this.generateInternalHandler(handlers)); + } + } + + /** + * This routine uniquely identifies each message, then adds a general + * message listener that waits for a response with the same id before resolving + * the promise. + */ + public emit = async <T = any>(name: string, args?: any): Promise<Response<T>> => { + if (this.isDestroyed) { + const error = { name: "FailedDispatch", message: "Cannot use a destroyed IPC manager to emit a message." }; + return { error }; + } + return new Promise<Response<T>>(resolve => { + const messageId = Utilities.guid(); + const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args }) => { + if (isResponse && id === messageId) { + this.target.removeListener("message", responseHandler); + resolve(args); + } + }; + this.target.addListener("message", responseHandler); + const message = { name, args, metadata: { id: messageId, isResponse: false } }; + if (!(this.target.send && this.target.send(message))) { + const error: ErrorLike = { name: "FailedDispatch", message: "Either the target's send method was undefined or the act of sending failed." }; + resolve({ error }); + this.target.removeListener("message", responseHandler); + } + }); + } + + /** + * Invoked from either the parent or the child process, this allows + * any unresolved promises to continue in the target process, but dispatches a dummy + * completion response for each of the pending messages, allowing their + * promises in the caller to resolve. + */ + public destroy = () => { + return new Promise<void>(async resolve => { + if (this.callerIsTarget) { + this.destroyHelper(); + } else { + await this.emit(destroyEvent); + } + resolve(); + }); + } + + /** + * Dispatches the dummy responses and sets the isDestroyed flag to true. + */ + private destroyHelper = () => { + const { pendingMessages } = this; + this.isDestroyed = true; + Object.keys(pendingMessages).forEach(id => { + const error: ErrorLike = { name: "ManagerDestroyed", message: "The IPC manager was destroyed before the response could be returned." }; + const message: InternalMessage = { name: pendingMessages[id], args: { error }, metadata: { id, isResponse: true } }; + this.target.send?.(message) + }); + this.pendingMessages = {}; + } + + /** + * This routine receives a uniquely identified message. If the message is itself a response, + * it is ignored to avoid infinite mutual responses. Otherwise, the routine awaits its completion using whatever + * router the caller has installed, and then sends a response containing the original message id, + * which will ultimately invoke the responseHandler of the original emission and resolve the + * sender's promise. + */ + private generateInternalHandler = (handlers: HandlerMap): MessageHandler => async (message: InternalMessage) => { + const { name, args, metadata } = message; + if (name && metadata && !metadata.isResponse) { + const { id } = metadata; + this.pendingMessages[id] = name; + let error: Error | undefined; + let results: any[] | undefined; + try { + const registered = handlers[name]; + if (registered) { + results = await Promise.all(registered.map(handler => handler(args))); + } + } catch (e) { + error = e; + } + if (!this.isDestroyed && this.target.send) { + const metadata = { id, isResponse: true }; + const response: Response = { results , error }; + const message = { name, args: response , metadata }; + delete this.pendingMessages[id]; + this.target.send(message); + } + } + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts new file mode 100644 index 000000000..976d27226 --- /dev/null +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -0,0 +1,160 @@ +import { ExitHandler } from "./applied_session_agent"; +import { isMaster } from "cluster"; +import { manage } from "./promisified_ipc_manager"; +import IPCMessageReceiver from "./process_message_router"; +import { red, green, white, yellow } from "colors"; +import { get } from "request-promise"; +import { Monitor } from "./monitor"; + +/** + * Effectively, each worker repairs the connection to the server by reintroducing a consistent state + * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification + * email if the server encounters an uncaught exception or if the server cannot be reached. + */ +export class ServerWorker extends IPCMessageReceiver { + private static count = 0; + private shouldServerBeResponsive = false; + private exitHandlers: ExitHandler[] = []; + private pollingFailureCount = 0; + private pollingIntervalSeconds: number; + private pollingFailureTolerance: number; + private pollTarget: string; + private serverPort: number; + private isInitialized = false; + + public static Create(work: Function) { + if (isMaster) { + console.error(red("cannot create a worker on the monitor process.")); + process.exit(1); + } else if (++ServerWorker.count > 1) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create more than one worker on a given worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else { + return new ServerWorker(work); + } + } + + /** + * Allows developers to invoke application specific logic + * by hooking into the exiting of the server process. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Kill the session monitor (parent process) from this + * server worker (child process). This will also kill + * this process (child process). + */ + public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit<never>("kill", { reason, graceful, errorCode }); + + /** + * A convenience wrapper to tell the session monitor (parent process) + * to carry out the action with the specified message and arguments. + */ + public emit = async <T = any>(name: string, args?: any) => ServerWorker.IPCManager.emit<T>(name, args); + + private constructor(work: Function) { + super(); + this.configureInternalHandlers(); + ServerWorker.IPCManager = manage(process, this.handlers); + this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); + + const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; + this.serverPort = Number(serverPort); + this.pollingIntervalSeconds = Number(pollingIntervalSeconds); + this.pollingFailureTolerance = Number(pollingFailureTolerance); + this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`; + + work(); + this.pollServer(); + } + + /** + * Set up message and uncaught exception handlers for this + * server process. + */ + protected configureInternalHandlers = () => { + // updates the local values of variables to the those sent from master + this.on("updatePollingInterval", ({ newPollingIntervalSeconds }) => this.pollingIntervalSeconds = newPollingIntervalSeconds); + this.on("manualExit", async ({ isSessionEnd }) => { + await ServerWorker.IPCManager.destroy(); + await this.executeExitHandlers(isSessionEnd); + process.exit(0); + }); + + // one reason to exit, as the process might be in an inconsistent state after such an exception + process.on('uncaughtException', this.proactiveUnplannedExit); + process.on('unhandledRejection', reason => { + const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); + this.proactiveUnplannedExit(appropriateError); + }); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Notify master thread (which will log update in the console) of initialization via IPC. + */ + public lifecycleNotification = (event: string) => this.emit("lifecycle", { event }); + + /** + * Called whenever the process has a reason to terminate, either through an uncaught exception + * in the process (potentially inconsistent state) or the server cannot be reached. + */ + private proactiveUnplannedExit = async (error: Error): Promise<void> => { + this.shouldServerBeResponsive = false; + // communicates via IPC to the master thread that it should dispatch a crash notification email + this.emit(Monitor.IntrinsicEvents.CrashDetected, { error }); + await this.executeExitHandlers(error); + // notify master thread (which will log update in the console) of crash event via IPC + this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`)); + this.lifecycleNotification(red(error.message)); + await ServerWorker.IPCManager.destroy(); + process.exit(1); + } + + /** + * This monitors the health of the server by submitting a get request to whatever port / route specified + * by the configuration every n seconds, where n is also given by the configuration. + */ + private pollServer = async (): Promise<void> => { + await new Promise<void>(resolve => { + setTimeout(async () => { + try { + await get(this.pollTarget); + if (!this.shouldServerBeResponsive) { + // notify monitor thread that the server is up and running + this.lifecycleNotification(green(`listening on ${this.serverPort}...`)); + this.emit(Monitor.IntrinsicEvents.ServerRunning, { isFirstTime: !this.isInitialized }); + this.isInitialized = true; + } + this.shouldServerBeResponsive = true; + } catch (error) { + // if we expect the server to be unavailable, i.e. during compilation, + // the listening variable is false, activeExit will return early and the child + // process will continue + if (this.shouldServerBeResponsive) { + if (++this.pollingFailureCount > this.pollingFailureTolerance) { + this.proactiveUnplannedExit(error); + } else { + this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`)); + } + } + } finally { + resolve(); + } + }, 1000 * this.pollingIntervalSeconds); + }); + // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed + this.pollServer(); + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/repl.ts b/src/server/DashSession/Session/utilities/repl.ts new file mode 100644 index 000000000..643141286 --- /dev/null +++ b/src/server/DashSession/Session/utilities/repl.ts @@ -0,0 +1,128 @@ +import { createInterface, Interface } from "readline"; +import { red, green, white } from "colors"; + +export interface Configuration { + identifier: () => string | string; + onInvalid?: (command: string, validCommand: boolean) => string | string; + onValid?: (success?: string) => string | string; + isCaseSensitive?: boolean; +} + +export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>; +export interface Registration { + argPatterns: RegExp[]; + action: ReplAction; +} + +export default class Repl { + private identifier: () => string | string; + private onInvalid: ((command: string, validCommand: boolean) => string) | string; + private onValid: ((success: string) => string) | string; + private isCaseSensitive: boolean; + private commandMap = new Map<string, Registration[]>(); + public interface: Interface; + private busy = false; + private keys: string | undefined; + + constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) { + this.identifier = prompt; + this.onInvalid = onInvalid || this.usage; + this.onValid = onValid || this.success; + this.isCaseSensitive = isCaseSensitive ?? true; + this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); + } + + private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier(); + + private usage = (command: string, validCommand: boolean) => { + if (validCommand) { + const formatted = white(command); + const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n')); + return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`; + } else { + const resolved = this.keys; + if (resolved) { + return resolved; + } + const members: string[] = []; + const keys = this.commandMap.keys(); + let next: IteratorResult<string>; + while (!(next = keys.next()).done) { + members.push(next.value); + } + return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`; + } + } + + private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`; + + public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + const existing = this.commandMap.get(basename); + const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); + const registration = { argPatterns: converted, action }; + if (existing) { + existing.push(registration); + } else { + this.commandMap.set(basename, [registration]); + } + } + + private invalid = (command: string, validCommand: boolean) => { + console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand))); + this.busy = false; + } + + private valid = (command: string) => { + console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command))); + this.busy = false; + } + + private considerInput = async (line: string) => { + if (this.busy) { + console.log(red("Busy")); + return; + } + this.busy = true; + line = line.trim(); + if (this.isCaseSensitive) { + line = line.toLowerCase(); + } + const [command, ...args] = line.split(/\s+/g); + if (!command) { + return this.invalid(command, false); + } + const registered = this.commandMap.get(command); + if (registered) { + const { length } = args; + const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); + for (const { argPatterns, action } of candidates) { + const parsed: string[] = []; + let matched = true; + if (length) { + for (let i = 0; i < length; i++) { + let matches: RegExpExecArray | null; + if ((matches = argPatterns[i].exec(args[i])) === null) { + matched = false; + break; + } + parsed.push(matches[0]); + } + } + if (!length || matched) { + const result = action(parsed); + const resolve = () => this.valid(`${command} ${parsed.join(" ")}`); + if (result instanceof Promise) { + result.then(resolve); + } else { + resolve(); + } + return; + } + } + this.invalid(command, true); + } else { + this.invalid(command, false); + } + } + +}
\ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts new file mode 100644 index 000000000..b0e65dde4 --- /dev/null +++ b/src/server/DashSession/Session/utilities/session_config.ts @@ -0,0 +1,129 @@ +import { Schema } from "jsonschema"; +import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors"; + +const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/; + +const identifierProperties: Schema = { + type: "object", + properties: { + text: { + type: "string", + minLength: 1 + }, + color: { + type: "string", + pattern: colorPattern + } + } +}; + +const portProperties: Schema = { + type: "number", + minimum: 1024, + maximum: 65535 +}; + +export const configurationSchema: Schema = { + id: "/configuration", + type: "object", + properties: { + showServerOutput: { type: "boolean" }, + ports: { + type: "object", + properties: { + server: portProperties, + socket: portProperties + }, + required: ["server"], + additionalProperties: true + }, + identifiers: { + type: "object", + properties: { + master: identifierProperties, + worker: identifierProperties, + exec: identifierProperties + } + }, + polling: { + type: "object", + additionalProperties: false, + properties: { + intervalSeconds: { + type: "number", + minimum: 1, + maximum: 86400 + }, + route: { + type: "string", + pattern: /\/[a-zA-Z]*/g + }, + failureTolerance: { + type: "number", + minimum: 0, + } + } + }, + } +}; + +type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; + +export const colorMapping: Map<ColorLabel, Color> = new Map([ + ["yellow", yellow], + ["red", red], + ["cyan", cyan], + ["green", green], + ["blue", blue], + ["magenta", magenta], + ["grey", grey], + ["gray", gray], + ["white", white], + ["black", black] +]); + +interface Identifier { + text: string; + color: ColorLabel; +} + +export interface Identifiers { + master: Identifier; + worker: Identifier; + exec: Identifier; +} + +export interface Configuration { + showServerOutput: boolean; + identifiers: Identifiers; + ports: { [description: string]: number }; + polling: { + route: string; + intervalSeconds: number; + failureTolerance: number; + }; +} + +export const defaultConfig: Configuration = { + showServerOutput: false, + identifiers: { + master: { + text: "__monitor__", + color: "yellow" + }, + worker: { + text: "__server__", + color: "magenta" + }, + exec: { + text: "__exec__", + color: "green" + } + }, + ports: { server: 3000 }, + polling: { + route: "/", + intervalSeconds: 30, + failureTolerance: 0 + } +};
\ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/utilities.ts b/src/server/DashSession/Session/utilities/utilities.ts new file mode 100644 index 000000000..eb8de9d7e --- /dev/null +++ b/src/server/DashSession/Session/utilities/utilities.ts @@ -0,0 +1,37 @@ +import { v4 } from "uuid"; + +export namespace Utilities { + + export function guid() { + return v4(); + } + + /** + * At any arbitrary layer of nesting within the configuration objects, any single value that + * is not specified by the configuration is given the default counterpart. If, within an object, + * one peer is given by configuration and two are not, the one is preserved while the two are given + * the default value. + * @returns the composition of all of the assigned objects, much like Object.assign(), but with more + * granularity in the overwriting of nested objects + */ + export function preciseAssign(target: any, ...sources: any[]): any { + for (const source of sources) { + preciseAssignHelper(target, source); + } + return target; + } + + export function preciseAssignHelper(target: any, source: any) { + Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { + let targetValue: any, sourceValue: any; + if (sourceValue = source[property]) { + if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { + preciseAssignHelper(targetValue, sourceValue); + } else { + target[property] = sourceValue; + } + } + }); + } + +}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 2101de1d2..88f5fa3bf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -24,7 +24,7 @@ import { Logger } from "./ProcessFactory"; import { yellow } from "colors"; import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import SessionManager from "./ApiManagers/SessionManager"; -import { AppliedSessionAgent } from "resilient-server-session"; +import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; export const onWindows = process.platform === "win32"; export let sessionAgent: AppliedSessionAgent; |