UNPKG

cn-shell

Version:
615 lines (614 loc) 17.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? function (o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if ( !desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable) ) { desc = { enumerable: true, get: function () { return m[k]; }, }; } Object.defineProperty(o, k2, desc); } : function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? function (o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); } : function (o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Shell = exports.undici = exports.ConfigTypes = exports.ConfigMan = exports.ShellExt = exports.LogLevel = exports.Logger = void 0; const logger_js_1 = require("./logger.js"); Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_js_1.Logger; }, }); Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_js_1.LogLevel; }, }); const logger_console_js_1 = require("./logger-console.js"); const shell_ext_js_1 = require("./shell-ext.js"); Object.defineProperty(exports, "ShellExt", { enumerable: true, get: function () { return shell_ext_js_1.ShellExt; }, }); const config_man_js_1 = require("./config-man.js"); Object.defineProperty(exports, "ConfigMan", { enumerable: true, get: function () { return config_man_js_1.ConfigMan; }, }); Object.defineProperty(exports, "ConfigTypes", { enumerable: true, get: function () { return config_man_js_1.ConfigTypes; }, }); const shelljs_1 = __importDefault(require("shelljs")); const undici_1 = require("undici"); const undici = __importStar(require("undici")); exports.undici = undici; const http = __importStar(require("node:http")); const os = __importStar(require("node:os")); const readline = __importStar(require("node:readline")); const CFG_LOG_LEVEL = "LOG_LEVEL"; const CFG_LOG_TIMESTAMP = "LOG_TIMESTAMP"; const CFG_LOG_TIMESTAMP_FORMAT = "LOG_TIMESTAMP_FORMAT"; const CFG_HTTP_KEEP_ALIVE_TIMEOUT = "HTTP_KEEP_ALIVE_TIMEOUT"; const CFG_HTTP_HEADER_TIMEOUT = "HTTP_HEADER_TIMEOUT"; const CFG_HTTP_HEALTHCHECK_PORT = "HTTP_HEALTHCHECK_PORT"; const CFG_HTTP_HEALTHCHECK_INTERFACE = "HTTP_HEALTHCHECK_INTERFACE"; const CFG_HTTP_HEALTHCHECK_PATH = "HTTP_HEALTHCHECK_PATH"; const CFG_HTTP_HEALTHCHECK_GOOD_RES = "HTTP_HEALTHCHECK_GOOD_RES"; const CFG_HTTP_HEALTHCHECK_BAD_RES = "HTTP_HEALTHCHECK_BAD_RES"; const DEFAULT_SHELL_CONFIG = { appVersion: "N/A", log: { level: "INFO", timestamp: false, timestampFormat: "ISO", }, http: { keepAliveTimeout: 65000, headerTimeout: 66000, healthcheckPort: 8080, healthcheckInterface: "", healthcheckPath: "/healthcheck", healthcheckGoodRes: 200, healthcheckBadRes: 503, }, }; const DEFAULT_CONFIG_OPTIONS = { envVarPrefix: "CNA_", }; const DEFAULT_QUESTION_OPTIONS = { muteAnswer: false, muteChar: "*", }; const DEFAULT_HTTP_REQ_POOL_OPTIONS = {}; const DEFAULT_HTTP_REQ_OPTIONS = { method: "GET", }; const NODE_ENV = process.env.NODE_ENV === undefined ? "development" : process.env.NODE_ENV; const LOGGER_APP_NAME = "App"; class Shell { _name; _appVersion; _configMan; _logger; _httpKeepAliveTimeout; _httpHeaderTimeout; _healthcheckPort; _healthcheckInterface; _healthCheckPath; _healthCheckGoodResCode; _healthCheckBadResCode; _healthcheckServer; _exts; _httpReqPools; cat = shelljs_1.default.cat; cd = shelljs_1.default.cd; chmod = shelljs_1.default.chmod; cp = shelljs_1.default.cp; dirs = shelljs_1.default.dirs; echo = shelljs_1.default.echo; env = shelljs_1.default.env; testError = shelljs_1.default.error; exec = shelljs_1.default.exec; find = shelljs_1.default.find; grep = shelljs_1.default.grep; head = shelljs_1.default.head; ln = shelljs_1.default.ln; ls = shelljs_1.default.ls; mkdir = shelljs_1.default.mkdir; mv = shelljs_1.default.mv; popd = shelljs_1.default.popd; pushd = shelljs_1.default.pushd; pwd = shelljs_1.default.pwd; rm = shelljs_1.default.rm; sed = shelljs_1.default.sed; set = shelljs_1.default.set; sort = shelljs_1.default.sort; tail = shelljs_1.default.tail; tempdir = shelljs_1.default.tempdir; touch = shelljs_1.default.touch; uniq = shelljs_1.default.uniq; constructor(passedConfig) { let config = { name: passedConfig.name, appVersion: passedConfig.appVersion === undefined ? DEFAULT_SHELL_CONFIG.appVersion : passedConfig.appVersion, http: { ...DEFAULT_SHELL_CONFIG.http, ...passedConfig.http, }, log: { ...DEFAULT_SHELL_CONFIG.log, ...passedConfig.log, }, }; this._name = config.name; this._appVersion = config.appVersion; this._configMan = new config_man_js_1.ConfigMan(); this._exts = []; this._httpReqPools = {}; this.ls = shelljs_1.default.ls; if (config.log?.logger !== undefined) { this._logger = config.log.logger; } else { let logTimestamps = this.getConfigBool(CFG_LOG_TIMESTAMP, { defaultVal: config.log.timestamp, }); let logTimestampFormat = this.getConfigStr(CFG_LOG_TIMESTAMP_FORMAT, { defaultVal: config.log.timestampFormat, }); this._logger = new logger_console_js_1.LoggerConsole( config.name, logTimestamps, logTimestampFormat, ); } this._logger.start(); let logLevel = this.getConfigStr(CFG_LOG_LEVEL, { defaultVal: config.log.level, }); switch (logLevel.toUpperCase()) { case "SILENT": this._logger.level = logger_js_1.LogLevel.LOG_COMPLETE_SILENCE; break; case "QUIET": this._logger.level = logger_js_1.LogLevel.LOG_QUIET; break; case "INFO": this._logger.level = logger_js_1.LogLevel.LOG_INFO; break; case "STARTUP": this._logger.level = logger_js_1.LogLevel.LOG_START_UP; break; case "DEBUG": this._logger.level = logger_js_1.LogLevel.LOG_DEBUG; break; case "TRACE": this._logger.level = logger_js_1.LogLevel.LOG_TRACE; break; default: this._logger.level = logger_js_1.LogLevel.LOG_INFO; this._logger.warn( `LogLevel ${logLevel} is unknown. Setting level to INFO.`, ); break; } this._healthcheckInterface = this.getConfigStr( CFG_HTTP_HEALTHCHECK_INTERFACE, { defaultVal: config.http.healthcheckInterface }, ); this._healthcheckPort = this.getConfigNum(CFG_HTTP_HEALTHCHECK_PORT, { defaultVal: config.http.healthcheckPort, }); this._httpKeepAliveTimeout = this.getConfigNum( CFG_HTTP_KEEP_ALIVE_TIMEOUT, { defaultVal: config.http.keepAliveTimeout }, ); this._httpHeaderTimeout = this.getConfigNum(CFG_HTTP_HEADER_TIMEOUT, { defaultVal: config.http.headerTimeout, }); this._healthCheckPath = this.getConfigStr(CFG_HTTP_HEALTHCHECK_PATH, { defaultVal: config.http.healthcheckPath, }); this._healthCheckGoodResCode = this.getConfigNum( CFG_HTTP_HEALTHCHECK_GOOD_RES, { defaultVal: config.http.healthcheckGoodRes }, ); this._healthCheckBadResCode = this.getConfigNum( CFG_HTTP_HEALTHCHECK_BAD_RES, { defaultVal: config.http.healthcheckBadRes }, ); this.startup("Shell created!"); } async start() { this.startup("Started!"); return true; } async stop() { this.startup("Stopped!"); return; } async healthCheck() { this.debug("Health check called"); return true; } get name() { return this._name; } get appVersion() { return this._appVersion; } get logger() { return this._logger; } set level(level) { this._logger.level = level; } setupHealthcheck() { if (this._healthcheckInterface.length === 0) { this.startup( "No HTTP interface specified for healthcheck endpoint - healthcheck disabled!", ); return; } this.startup("Initialising healthcheck HTTP endpoint ..."); this.startup(`Finding IP for interface (${this._healthcheckInterface})`); let ifaces = os.networkInterfaces(); this.startup("Interfaces on host: %j", ifaces); if (ifaces[this._healthcheckInterface] === undefined) { throw new Error( `${this._healthcheckInterface} is not an interface on this server`, ); } let ip = ""; let found = ifaces[this._healthcheckInterface]?.find( (i) => i.family === "IPv4", ); if (found !== undefined) { ip = found.address; this.startup( `Found IP (${ip}) for interface ${this._healthcheckInterface}`, ); this.startup( `Will listen on interface ${this._healthcheckInterface} (IP: ${ip})`, ); } if (ip.length === 0) { throw new Error( `${this._healthcheckInterface} is not an interface on this server`, ); } this.startup( `Attempting to listen on (http://${ip}:${this._healthcheckPort})`, ); this._healthcheckServer = http .createServer((req, res) => this.healthcheckCallback(req, res)) .listen(this._healthcheckPort, ip); this._healthcheckServer.keepAliveTimeout = this._httpKeepAliveTimeout; this._healthcheckServer.headersTimeout = this._httpHeaderTimeout; this.startup("Now listening. Healthcheck endpoint enabled!"); } async startupError(code, testing) { this.error("Heuston, we have a problem. Shutting down now ..."); if (testing) { await this.exit(code, false); return; } await this.exit(code); } async init(testing = false) { this.startup("Initialising ..."); this.startup(`App Version (${this._appVersion})`); this.startup(`NODE_ENV (${NODE_ENV})`); for (let ext of this._exts) { this.startup(`Attempting to start extension ${ext.name} ...`); await ext.start().catch(async (e) => { this.error(e); await this.startupError(-1, testing); }); } this.startup("Attempting to start the application ..."); await this.start().catch(async (e) => { this.error(e); await this.startupError(-1, testing); }); await this.setupHealthcheck(); this.startup("Setting up event handler for SIGINT and SIGTERM"); process.on("SIGINT", async () => await this.exit(0)); process.on("SIGTERM", async () => await this.exit(0)); this.startup("Ready to Rock and Roll baby!"); } async exit(code, hard = true) { this.startup("Exiting ..."); if (this._healthcheckServer !== undefined) { this.startup("Closing healthcheck endpoint port now ..."); this._healthcheckServer.close(); this.startup("Port closed"); } this.startup("Attempting to stop the application ..."); await this.stop().catch((e) => { this.error(e); }); for (let ext of this._exts.reverse()) { this.startup(`Attempting to stop extension ${ext.name} ...`); await ext.stop().catch((e) => { this.error(e); }); } for (let origin in this._httpReqPools) { this._httpReqPools[origin].destroy(); } this.startup("So long and thanks for all the fish!"); this._logger.stop(); if (hard) { process.exit(code); } } getConfigStr(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) { let options = { ...DEFAULT_CONFIG_OPTIONS, ...passedOptions, }; return this._configMan.get( config, config_man_js_1.ConfigTypes.String, options, appOrExtName, this._logger, ); } getConfigBool(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) { let options = { ...DEFAULT_CONFIG_OPTIONS, ...passedOptions, }; return this._configMan.get( config, config_man_js_1.ConfigTypes.Boolean, options, appOrExtName, this._logger, ); } getConfigNum(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) { let options = { ...DEFAULT_CONFIG_OPTIONS, ...passedOptions, }; return this._configMan.get( config, config_man_js_1.ConfigTypes.Number, options, appOrExtName, this._logger, ); } async healthcheckCallback(req, res) { if ( req.method?.toLowerCase() !== "get" || req.url !== this._healthCheckPath ) { res.statusCode = 404; res.end(); return; } let healthy = await this.healthCheck().catch((e) => { this.error(e); }); if (healthy) { res.statusCode = this._healthCheckGoodResCode; } else { res.statusCode = this._healthCheckBadResCode; } res.end(); } fatal(...args) { this._logger.fatal(LOGGER_APP_NAME, ...args); } error(...args) { this._logger.error(LOGGER_APP_NAME, ...args); } warn(...args) { this._logger.warn(LOGGER_APP_NAME, ...args); } info(...args) { this._logger.info(LOGGER_APP_NAME, ...args); } startup(...args) { this._logger.startup(LOGGER_APP_NAME, ...args); } debug(...args) { this._logger.debug(LOGGER_APP_NAME, ...args); } trace(...args) { this._logger.trace(LOGGER_APP_NAME, ...args); } force(...args) { this._logger.force(LOGGER_APP_NAME, ...args); } addExt(ext) { this.startup(`Adding extension ${ext.name}`); this._exts.push(ext); } async sleep(durationInSeconds) { let ms = Math.round(durationInSeconds * 1000); return new Promise((resolve) => { setTimeout(resolve, ms); }); } async question(ask, passedOptions) { let input = process.stdin; let output = process.stdout; let options = { ...DEFAULT_QUESTION_OPTIONS, ...passedOptions, }; return new Promise((resolve) => { let rl = readline.createInterface({ input, output, }); if (options.muteAnswer) { input.on("keypress", () => { var len = rl.line.length; if (options.muteChar.length === 0) { readline.moveCursor(output, -1, 0); readline.clearLine(output, 1); } else { readline.moveCursor(output, -len, 0); readline.clearLine(output, 1); for (var i = 0; i < len; i++) { output.write(options.muteChar[0]); } } }); } rl.question(ask, (answer) => { resolve(answer); rl.close(); }); }); } createHttpReqPool(origin, passedOptions) { let options = { ...DEFAULT_HTTP_REQ_POOL_OPTIONS, ...passedOptions, }; this.trace( "Creating new HTTP pool for (%s) with options (%j)", origin, passedOptions, ); if (this._httpReqPools[origin] !== undefined) { throw new Error(`A HTTP pool already exists for ${origin}`); } this._httpReqPools[origin] = new undici_1.Pool(origin, options); } async httpReq(origin, path, passedOptions) { let options = { ...DEFAULT_HTTP_REQ_OPTIONS, ...passedOptions, }; this.trace("httpReq for origin (%s) path (%s)", origin, path); let pool = this._httpReqPools[origin]; if (pool === undefined) { this.createHttpReqPool(origin); pool = this._httpReqPools[origin]; } let headers = options.headers === undefined ? {} : options.headers; if (options.bearerToken !== undefined) { headers.Authorization = `Bearer ${options.bearerToken}`; } if (options.auth !== undefined) { let token = Buffer.from( `${options.auth.username}:${options.auth.password}`, ).toString("base64"); headers.Authorization = `Basic ${token}`; } let body; if (options.body !== undefined && options.method !== "GET") { if ( options.headers?.["content-type"] === undefined || typeof options.body === "object" ) { headers["content-type"] = "application/json; charset=utf-8"; body = JSON.stringify(options.body); } else { body = options.body; } } let results = await pool.request({ origin, path, method: options.method, headers, query: options.searchParams, body, }); let resData; let contentExists = false; if ( results.headers["content-length"] !== undefined && results.headers["content-length"] !== "0" ) { contentExists = true; } if ( contentExists && results.headers["content-type"]?.startsWith("application/json") === true ) { resData = await results.body.json(); } else { resData = await results.body.text(); if ( resData.length && results.headers["content-type"]?.startsWith("application/json") === true ) { resData = JSON.parse(resData); } } let res = { statusCode: results.statusCode, headers: results.headers, trailers: results.trailers, body: resData, }; return res; } } exports.Shell = Shell;