diff options
-rw-r--r-- | package-lock.json | 410 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/server/ApiManagers/SessionManager.ts | 29 | ||||
-rw-r--r-- | src/server/DashSession/DashSessionAgent.ts | 56 | ||||
-rw-r--r-- | src/server/index.ts | 2 | ||||
-rw-r--r-- | src/server/session/README.txt | 11 | ||||
-rw-r--r-- | src/server/session/agents/applied_session_agent.ts | 58 | ||||
-rw-r--r-- | src/server/session/agents/monitor.ts | 302 | ||||
-rw-r--r-- | src/server/session/agents/process_message_router.ts | 46 | ||||
-rw-r--r-- | src/server/session/agents/promisified_ipc_manager.ts | 97 | ||||
-rw-r--r-- | src/server/session/agents/server_worker.ts | 160 | ||||
-rw-r--r-- | src/server/session/utilities/repl.ts | 128 | ||||
-rw-r--r-- | src/server/session/utilities/session_config.ts | 129 | ||||
-rw-r--r-- | src/server/session/utilities/utilities.ts | 31 | ||||
-rw-r--r-- | src/typings/index.d.ts | 2 |
15 files changed, 378 insertions, 1086 deletions
diff --git a/package-lock.json b/package-lock.json index 1200dc26c..3b4fbcc29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,8 +374,7 @@ "@types/chai": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", - "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==", - "dev": true + "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==" }, "@types/classnames": { "version": "2.2.9", @@ -595,8 +594,7 @@ "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" }, "@types/mongodb": { "version": "3.3.14", @@ -2444,21 +2442,6 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, - "boolify-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/boolify-string/-/boolify-string-2.0.2.tgz", - "integrity": "sha1-n4m9l9YKFEijlAF8SjuaPSQNRY4=", - "requires": { - "type-detect": "^1.0.0" - }, - "dependencies": { - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=" - } - } - }, "bootstrap": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz", @@ -2678,7 +2661,8 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, "buffer-indexof": { "version": "1.1.1", @@ -4303,14 +4287,6 @@ "minimalistic-crypto-utils": "^1.0.0" } }, - "emit-logger": { - "version": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070", - "from": "github:chocolateboy/emit-logger#better-emitter-name", - "requires": { - "chalk": "^1.1.1", - "moment": "^2.10.6" - } - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -5947,11 +5923,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, - "get-them-args": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz", - "integrity": "sha1-dKILqKSr7OWuGZrQPyvMaP38m6U=" - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -6917,20 +6888,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, - "ipc-event-emitter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ipc-event-emitter/-/ipc-event-emitter-2.0.2.tgz", - "integrity": "sha512-hJsN8zCg8MZwl5nbTutqINDO4pJPbKwmCfrTJaRLNE+5H15mJx7Mxo3pXIAi8zlh+N5xpf+PdMOQ0pbtZQvRKA==", - "requires": { - "babel-runtime": "^6.22.0", - "bluebird": "^3.4.7", - "boolify-string": "^2.0.2", - "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name", - "lodash": "^4.17.4", - "semver": "^5.0.3", - "source-map-support": "^0.5.9" - } - }, "is-absolute-url": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", @@ -7596,15 +7553,6 @@ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" }, - "kill-port": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/kill-port/-/kill-port-1.6.0.tgz", - "integrity": "sha512-gwHRBZ3OLBcupsOJZlIt2Xvf6QqFH3lfdpGnmonXJnJrqq819UXtItGEU1rCMXHK6sXFlxdpkw8ka56rtWw/eQ==", - "requires": { - "get-them-args": "^1.3.1", - "shell-exec": "^1.0.2" - } - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -8290,11 +8238,6 @@ } } }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, "mongodb": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz", @@ -12494,6 +12437,11 @@ "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", @@ -13767,7 +13715,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -14088,6 +14036,331 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resilient-server-session": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/resilient-server-session/-/resilient-server-session-1.1.2.tgz", + "integrity": "sha512-eLoXxTc5bFOYH2JejSCYc2O8emoo80p2zOuwVVWVoK6/2NJBzLP8Yl7kU8m7tJDrOoqDSZGghsVD5ob9BbUgAQ==", + "requires": { + "@types/chai": "^4.2.7", + "@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", + "jsonschema": "^1.2.5", + "mocha": "^7.0.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.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz", + "integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==", + "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", @@ -14599,11 +14872,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, - "shell-exec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shell-exec/-/shell-exec-1.0.2.tgz", - "integrity": "sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==" - }, "shelljs": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", @@ -15001,6 +15269,7 @@ "version": "0.5.12", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15009,7 +15278,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -15344,7 +15614,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -17549,7 +17819,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", diff --git a/package.json b/package.json index 0a6cada2b..be1d3a9fe 100644 --- a/package.json +++ b/package.json @@ -157,13 +157,11 @@ "image-data-uri": "^2.0.1", "image-size": "^0.7.5", "imagesloaded": "^4.1.4", - "ipc-event-emitter": "^2.0.2", "jquery-awesome-cursor": "^0.3.1", "js-datepicker": "^4.6.6", "jsonschema": "^1.2.5", "jsonwebtoken": "^8.5.0", "jsx-to-string": "^1.4.0", - "kill-port": "^1.6.0", "lodash": "^4.17.15", "mobile-detect": "^1.4.4", "mobx": "^5.15.1", @@ -218,6 +216,7 @@ "readline": "^1.3.0", "request": "^2.88.0", "request-promise": "^4.2.5", + "resilient-server-session": "^1.1.2", "rimraf": "^3.0.0", "serializr": "^1.5.4", "sharp": "^0.23.4", diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index d989d8d1b..a99aa05e0 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -28,22 +28,11 @@ export default class SessionManager extends ApiManager { register({ method: Method.GET, - subscription: this.secureSubscriber("debug", "mode?", "recipient?"), - secureHandler: this.authorizedAction(async ({ req, res }) => { - const { mode, recipient } = req.params; - if (mode && !["passive", "active"].includes(mode)) { - res.send(`Your request failed. '${mode}' is not a valid mode: please choose either 'active' or 'passive'`); - } else { - const response = await sessionAgent.serverWorker.emitToMonitorPromise("debug", { - mode: mode || "active", - recipient: recipient || DashSessionAgent.notificationRecipient - }); - if (response instanceof Error) { - res.send(response); - } else { - res.send(`Your request was successful: the server ${mode === "active" ? "created and compressed a new" : "retrieved and compressed the most recent"} back up. It was sent to ${recipient}.`); - } - } + subscription: this.secureSubscriber("debug", "to?"), + secureHandler: this.authorizedAction(async ({ req: { params }, res }) => { + const to = params.to || DashSessionAgent.notificationRecipient; + const { error } = await sessionAgent.serverWorker.emit("debug", { to }); + res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`); }) }); @@ -51,12 +40,8 @@ export default class SessionManager extends ApiManager { method: Method.GET, subscription: this.secureSubscriber("backup"), secureHandler: this.authorizedAction(async ({ res }) => { - const response = await sessionAgent.serverWorker.emitToMonitor("backup"); - if (response instanceof Error) { - res.send(response); - } else { - res.send("Your request was successful: the server successfully created a new back up."); - } + const { error } = await sessionAgent.serverWorker.emit("backup"); + res.send(error ? error.message : "Your request was successful: the server successfully created a new back up."); }) }); diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index fe7cdae88..c55e01243 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -5,13 +5,11 @@ import { Utils } from "../../Utils"; import { WebSocket } from "../Websocket/Websocket"; import { MessageStore } from "../Message"; import { launchServer, onWindows } from ".."; -import { existsSync, mkdirSync, readdirSync, statSync, createWriteStream, readFileSync } from "fs"; +import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; import * as Archiver from "archiver"; import { resolve } from "path"; -import { AppliedSessionAgent, ExitHandler } from "../session/agents/applied_session_agent"; -import { Monitor } from "../session/agents/monitor"; -import { ServerWorker } from "../session/agents/server_worker"; -import { MessageHandler } from "../session/agents/promisified_ipc_manager"; +import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; +import rimraf = require("rimraf"); /** * If we're the monitor (master) thread, we should launch the monitor logic for the session. @@ -27,14 +25,14 @@ export class DashSessionAgent extends AppliedSessionAgent { * The core method invoked when the single master thread is initialized. * Installs event hooks, repl commands and additional IPC listeners. */ - protected async initializeMonitor(monitor: Monitor, sessionKey: string) { + protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise<void> { await this.dispatchSessionPassword(sessionKey); monitor.addReplCommand("pull", [], () => monitor.exec("git pull")); monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand); monitor.addReplCommand("backup", [], this.backup); - monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async ([mode, recipient]) => this.dispatchZippedDebugBackup(mode, recipient)); + monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); monitor.on("backup", this.backup); - monitor.on("debug", ({ mode, recipient }) => this.dispatchZippedDebugBackup(mode, recipient)); + monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); } @@ -42,7 +40,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * The core method invoked when a server worker thread is initialized. * Installs logic to be executed when the server worker dies. */ - protected async initializeServerWorker() { + protected async initializeServerWorker(): Promise<ServerWorker> { const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker worker.addExitHandler(this.notifyClient); return worker; @@ -52,7 +50,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally. */ private _remoteDebugInstructions: string | undefined; - private generateDebugInstructions = (zipName: string, target: string) => { + private generateDebugInstructions = (zipName: string, target: string): string => { if (!this._remoteDebugInstructions) { this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" }); } @@ -66,7 +64,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * Prepares the body of the email with information regarding a crash event. */ private _crashInstructions: string | undefined; - private generateCrashInstructions({ name, message, stack }: Error) { + private generateCrashInstructions({ name, message, stack }: Error): string { if (!this._crashInstructions) { this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" }); } @@ -81,14 +79,18 @@ export class DashSessionAgent extends AppliedSessionAgent { * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone * to kill the server via the /kill/:key route. */ - private dispatchSessionPassword = async (sessionKey: string) => { + private dispatchSessionPassword = async (sessionKey: string): Promise<void> => { const { mainLog } = this.sessionMonitor; const { notificationRecipient } = DashSessionAgent; mainLog(green("dispatching session key...")); const error = await Email.dispatch({ to: notificationRecipient, subject: "Dash Release Session Admin Authentication Key", - content: `Here's the key for this session (started @ ${new Date().toUTCString()}):\n\n${sessionKey}.\n\n${this.signature}` + content: [ + `Here's the key for this session (started @ ${new Date().toUTCString()}):`, + sessionKey, + this.signature + ].join("\n\n") }); if (error) { this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`)); @@ -121,7 +123,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * Logic for interfacing with Solr. Either starts it, * stops it, or rebuilds its indicies. */ - private executeSolrCommand = async (args: string[]) => { + private executeSolrCommand = async (args: string[]): Promise<void> => { const { exec, mainLog } = this.sessionMonitor; const action = args[0]; if (action === "index") { @@ -154,7 +156,7 @@ export class DashSessionAgent extends AppliedSessionAgent { * Performs a backup of the database, saved to the desktop subdirectory. * This should work as is only on our specific release server. */ - private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop }); + private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop }); /** * Compress either a brand new backup or the most recent backup and send it @@ -162,21 +164,14 @@ export class DashSessionAgent extends AppliedSessionAgent { * @param mode specifies whether or not to make a new backup before exporting * @param to the recipient of the email */ - private async dispatchZippedDebugBackup(mode: string, to: string) { + private async dispatchZippedDebugBackup(to: string): Promise<void> { const { mainLog } = this.sessionMonitor; try { // if desired, complete an immediate backup to send - if (mode === "active") { - await this.backup(); - mainLog("backup complete"); - } + await this.backup(); + mainLog("backup complete"); - // ensure the directory for compressed backups exists const backupsDirectory = `${this.releaseDesktop}/backups`; - const compressedDirectory = `${this.releaseDesktop}/compressed`; - if (!existsSync(compressedDirectory)) { - mkdirSync(compressedDirectory); - } // sort all backups by their modified time, and choose the most recent one const target = readdirSync(backupsDirectory).map(filename => ({ @@ -187,11 +182,12 @@ export class DashSessionAgent extends AppliedSessionAgent { // create a zip file and to it, write the contents of the backup directory const zipName = `${target}.zip`; - const zipPath = `${compressedDirectory}/${zipName}`; + const zipPath = `${this.releaseDesktop}/${zipName}`; + const targetPath = `${backupsDirectory}/${target}`; const output = createWriteStream(zipPath); const zip = Archiver('zip'); zip.pipe(output); - zip.directory(`${backupsDirectory}/${target}/Dash`, false); + zip.directory(`${targetPath}/Dash`, false); await zip.finalize(); mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`); @@ -203,6 +199,12 @@ export class DashSessionAgent extends AppliedSessionAgent { attachments: [{ filename: zipName, path: zipPath }] }); + // since this is intended to be a zero-footprint operation, clean up + // by unlinking both the backup generated earlier in the function and the compressed zip file. + // to generate a persistent backup, just run backup. + unlinkSync(zipPath); + rimraf.sync(targetPath); + // indicate success or failure mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`); error && mainLog(red(error.message)); diff --git a/src/server/index.ts b/src/server/index.ts index ef8ed9700..313a2f0e2 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 "./session/agents/applied_session_agent"; +import { AppliedSessionAgent } from "resilient-server-session"; export const onWindows = process.platform === "win32"; export let sessionAgent: AppliedSessionAgent; diff --git a/src/server/session/README.txt b/src/server/session/README.txt deleted file mode 100644 index ac7d3d4e7..000000000 --- a/src/server/session/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -/** - * These abstractions rely on NodeJS's cluster module, which allows a parent (master) process to share - * code with its children (workers). A simple `isMaster` flag indicates who is trying to access - * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally). - * - * Think of the master thread as a factory, and the workers as the helpers that actually run the server. - * - * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process - * This will spawn off its own child process (by default, mirrors the execution path of its parent), - * in which initializeWorker() is invoked. - */
\ No newline at end of file diff --git a/src/server/session/agents/applied_session_agent.ts b/src/server/session/agents/applied_session_agent.ts deleted file mode 100644 index 48226dab6..000000000 --- a/src/server/session/agents/applied_session_agent.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { isMaster } from "cluster"; -import { Monitor } from "./monitor"; -import { ServerWorker } from "./server_worker"; -import { Utils } from "../../../Utils"; - -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, key: string): Promise<void>; - 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.emitToMonitor("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) { - const sessionKey = Utils.GenerateGuid(); - await this.initializeMonitor(this.sessionMonitorRef = Monitor.Create(sessionKey), sessionKey); - this.sessionMonitorRef.finalize(); - } 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/session/agents/monitor.ts b/src/server/session/agents/monitor.ts deleted file mode 100644 index 5ea950b2b..000000000 --- a/src/server/session/agents/monitor.ts +++ /dev/null @@ -1,302 +0,0 @@ -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 { IPC_Promisify, 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 ProcessMessageRouter 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 ProcessMessageRouter { - 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(sessionKey: string) { - 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(sessionKey); - } - } - - private constructor(sessionKey: string) { - super(); - this.config = this.loadAndValidateConfiguration(); - this.initialize(sessionKey); - this.repl = this.initializeRepl(); - } - - private initialize = (sessionKey: string) => { - console.log(this.timestamp(), cyan("initializing session...")); - this.key = sessionKey; - - // 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"] }); - - // 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)}`); - } - } - }); - - // 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 = (): void => { - if (this.finalized) { - throw new Error("Session monitor is already finalized"); - } - this.finalized = true; - 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); - 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 = (graceful = true, isSessionEnd = false): void => { - if (this.activeWorker && !this.activeWorker.isDead()) { - if (graceful) { - Monitor.IPCManager.emit("manualExit", { isSessionEnd }); - } else { - 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 = (): void => { - const { - polling: { - route, - failureTolerance, - intervalSeconds - }, - ports - } = this.config; - this.killActiveWorker(); - this.activeWorker = fork({ - pollingRoute: route, - pollingFailureTolerance: failureTolerance, - serverPort: ports.server, - socketPort: ports.socket, - pollingIntervalSeconds: intervalSeconds, - session_key: this.key - }); - Monitor.IPCManager = IPC_Promisify(this.activeWorker, this.route); - this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`)); - - this.on("kill", ({ reason, graceful, errorCode }) => this.killSession(reason, graceful, errorCode), true); - this.on("lifecycle", ({ event }) => console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${event})`), true); - } - -} - -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/session/agents/process_message_router.ts b/src/server/session/agents/process_message_router.ts deleted file mode 100644 index d359e97c3..000000000 --- a/src/server/session/agents/process_message_router.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MessageHandler, PromisifiedIPCManager } from "./promisified_ipc_manager"; - -export default abstract class ProcessMessageRouter { - - protected static IPCManager: PromisifiedIPCManager; - private onMessage: { [name: string]: MessageHandler[] | undefined } = {}; - - /** - * 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, exclusive = false) => { - const handlers = this.onMessage[name]; - if (exclusive || !handlers) { - this.onMessage[name] = [handler]; - } else { - handlers.push(handler); - } - } - - /** - * Unregister a given listener at this message. - */ - public off = (name: string, handler: MessageHandler) => { - const handlers = this.onMessage[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 => this.onMessage[name] = undefined); - - protected route: MessageHandler = async ({ name, args }) => { - const handlers = this.onMessage[name]; - if (handlers) { - await Promise.all(handlers.map(handler => handler(args))); - } - } - -}
\ No newline at end of file diff --git a/src/server/session/agents/promisified_ipc_manager.ts b/src/server/session/agents/promisified_ipc_manager.ts deleted file mode 100644 index 216e9be44..000000000 --- a/src/server/session/agents/promisified_ipc_manager.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Utils } from "../../../Utils"; -import { isMaster } from "cluster"; - -/** - * Convenience constructor - * @param target the process / worker to which to attach the specialized listeners - */ -export function IPC_Promisify(target: IPCTarget, router: Router) { - return new PromisifiedIPCManager(target, router); -} - -/** - * Essentially, a node process or node cluster worker - */ -export type IPCTarget = NodeJS.EventEmitter & { send?: Function }; - -/** - * Some external code that maps the name of incoming messages to registered handlers, if any - * when this returns, the message is assumed to have been handled in its entirety by the process, so - * await any asynchronous code inside this router. - */ -export type Router = (message: Message) => void | Promise<void>; - -/** - * 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 - */ -type InternalMessage = Message & { metadata: any }; -type InternalMessageHandler = (message: InternalMessage) => any | Promise<any>; - -/** - * 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; - - constructor(target: IPCTarget, router: Router) { - this.target = target; - this.target.addListener("message", this.internalHandler(router)); - } - - /** - * A convenience wrapper around the standard process emission. - * Does not wait for a response. - */ - public emit = async (name: string, args?: any) => this.target.send?.({ name, args }); - - /** - * 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 emitPromise = async (name: string, args?: any) => { - return new Promise(resolve => { - const messageId = Utils.GenerateGuid(); - const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args, name }) => { - if (isResponse && id === messageId) { - this.target.removeListener("message", responseHandler); - resolve(args?.error as Error | undefined); - } - }; - this.target.addListener("message", responseHandler); - const message = { name, args, metadata: { id: messageId } }; - this.target.send?.(message); - }); - } - - /** - * 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 internalHandler = (router: Router) => async ({ name, args, metadata }: InternalMessage) => { - if (name && (!metadata || !metadata.isResponse)) { - let error: Error | undefined; - try { - await router({ name, args }); - } catch (e) { - error = e; - } - if (metadata && this.target.send) { - metadata.isResponse = true; - this.target.send({ name, args: { error }, metadata }); - } - } - } - -}
\ No newline at end of file diff --git a/src/server/session/agents/server_worker.ts b/src/server/session/agents/server_worker.ts deleted file mode 100644 index 705307030..000000000 --- a/src/server/session/agents/server_worker.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { ExitHandler } from "./applied_session_agent"; -import { isMaster } from "cluster"; -import { PromisifiedIPCManager } from "./promisified_ipc_manager"; -import ProcessMessageRouter 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 ProcessMessageRouter { - 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.emitToMonitor("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 emitToMonitor = (name: string, args?: any) => ServerWorker.IPCManager.emit(name, args); - - public emitToMonitorPromise = (name: string, args?: any) => ServerWorker.IPCManager.emitPromise(name, args); - - private constructor(work: Function) { - super(); - ServerWorker.IPCManager = new PromisifiedIPCManager(process, this.route); - 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}`; - - this.configureProcess(); - work(); - this.pollServer(); - } - - /** - * Set up message and uncaught exception handlers for this - * server process. - */ - private configureProcess = () => { - // 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 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) => ServerWorker.IPCManager.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.emitToMonitor(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)); - 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.emitToMonitor(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/session/utilities/repl.ts b/src/server/session/utilities/repl.ts deleted file mode 100644 index 643141286..000000000 --- a/src/server/session/utilities/repl.ts +++ /dev/null @@ -1,128 +0,0 @@ -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/session/utilities/session_config.ts b/src/server/session/utilities/session_config.ts deleted file mode 100644 index b0e65dde4..000000000 --- a/src/server/session/utilities/session_config.ts +++ /dev/null @@ -1,129 +0,0 @@ -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/session/utilities/utilities.ts b/src/server/session/utilities/utilities.ts deleted file mode 100644 index ac8a6590a..000000000 --- a/src/server/session/utilities/utilities.ts +++ /dev/null @@ -1,31 +0,0 @@ -export namespace Utilities { - - /** - * 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/typings/index.d.ts b/src/typings/index.d.ts index cc68e8a4d..281bb3217 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -2,8 +2,6 @@ declare module 'googlephotos'; declare module 'react-image-lightbox-with-rotate'; -declare module 'kill-port'; -declare module 'ipc-event-emitter'; declare module 'cors'; declare module '@react-pdf/renderer' { |