UNPKG

profoundjs

Version:

Profound.js Framework and Server

876 lines (800 loc) 26.2 kB
#!/usr/bin/env node "use strict"; /* * This is NPM run script 'completeInstall' * This script is intended to be run by the user at a shell after 'npm install', via 'complete_intall.js'. * Prompts for configuration values and other installation options, if necessary, and creates config.js. * Kicks off 'npm run setup' to complete installation. */ const child_process = require("child_process"); const fs = require("fs"); const minimist = require("minimist"); const os = require("os"); const path = require("path"); const readline = require("readline"); const iutils = require("./install_utils.js"); /* NOTE: install_info.json This file contains instructions for completing the installation. It tells the process what questions to ask and what commands to run. This file is shared with the GUI installer. If any changes are made to how install_info.json is processed in this script, the GUI installer will also need to be adjusted, retested, and updated. */ const install_info = require("./install_info.json"); const semver = require("semver"); const IBMi = iutils.isIBMi(); (async() => { try { const argDefs = { alias: { "h": "help", "c": "configure", "s": "silent", "v": "validate" }, boolean: [ "h", "c", "s", "v", "installSamples" ], string: [] }; // Parse arguments. let args = minimist(process.argv.slice(2), argDefs); // Show help and quit, if requested. if (args["help"]) { const HELP = `Usage: node complete_install.js [OPTION] -h, --help Print this help and exit. -c, --configure Update config.js, if present. -s, --silent No interactive prompts. -v, --validate Validate silent mode arguments only. --installSamples Install sample code Creates config.js, if not present, and completes installation. --configure can be specified to update an existing config.js before proceeding. Configuration values are read from interactive prompts by default. If --silent is specified, configuration values are read from arguments instead. --validate can be used to validate --silent mode arguments without installing. If arguments are not valid, the script will exit with code 2 and error messages will be output to stderr in JSON format. --installSamples installs sample modules, workspaces, plugins, and other sample code. Valid arguments for --silent mode: ` + await buildArgHelp(); console.log(HELP); process.exit(0); } // Add silent mode arguments, if necessary, and reparse. if (args["silent"] === true) { await forEachQuestion(question => { if (question.id) { const arr = question.type === "boolean" ? argDefs.boolean : argDefs.string; arr.push(question.id); } return true; }); } argDefs.unknown = function(arg) { console.error("complete_install.js: Unknown argument:", arg); process.exit(1); }; args = minimist(process.argv.slice(2), argDefs); // Validate arguments. if (args._.length > 0) { console.error("complete_install.js: Unknown argument:", args._[0]); process.exit(1); } if (args.validate === true) { if (args.silent !== true) { console.error("complete_install.js: --validate is not valid without --silent"); process.exit(1); } if (args.configure === true) { console.error("complete_install.js: --validate is not valid with --configure"); process.exit(1); } } const RED = args.silent === true ? "" : "\x1b[31m"; const RESET = args.silent === true ? "" : "\x1b[0m"; // Silent/validation mode. if (args.silent === true && args.validate === true) { const errors = {}; await forEachQuestion(async question => { if (argDefined(argDefs, args, question.id)) { const result = validate(question, args[question.id]); if (result !== true) { errors[question.id] = result; } } return true; }); if (Object.keys(errors).length > 0) { process.stderr.write(JSON.stringify(errors, null, 2) + "\n"); process.exit(2); } else { process.exit(0); } } const deployDir = iutils.getDeployDir(); if (!deployDir) { console.error("complete_install.js: Can't find deployment directory."); process.exit(1); } const nodeMajorVersion = Number(process.versions.node.split(".")[0]); const gitSupported = ["win32", "darwin", "linux"].includes(os.platform()); const configPath = iutils.getConfigPath(); // Note: because an eval() may set the "config" variable subsequently, "config" cannot be a const. // eslint-disable-next-line prefer-const let config = fileExists(configPath) ? require(configPath) : {}; const strtcpsvr_config = {}; let svrname, autostart, ccsid, nodePath, installSamples; // If config.js doesn't exist, or if --configure is passed, prompt and create/update config.js. // Else config.js does exist and not --configure and --silent, resolve srvname and autostart from arguments. // This allow the config file to already be configured but the ibmi service not yet be installed --> as with Transformation customers if (!fileExists(configPath) || args["configure"] === true) { const warnings = getWarnings(); for (const warning of warnings) { console.log(""); console.log(`${RED}WARNING - ${warning.text}${RESET}`); console.log(""); } if (IBMi) { // Load STRTCPSVR configuration. const serverInstances = iutils.getIBMiInstances(deployDir); if (serverInstances.length > 1) { console.log(""); console.log(`${RED}WARNING: Multiple STRTCPSVR/ENDTCPSVR instances found for this server in /profoundjs-base/instances:${RESET}`); console.log(""); serverInstances.forEach(serverInstance => { console.log(JSON.stringify(serverInstance)); }); } const serverInstance = serverInstances[0]; if (serverInstance) { strtcpsvr_config.svrname = serverInstance.name; strtcpsvr_config.autostart = false; await forEachQuestion(question => { if (question.id && question.id.startsWith("strtcpsvr_")) { const name = question.id.substring("strtcpsvr_".length); const opt = serverInstance.options.find(option => option.name === name); if (opt) { strtcpsvr_config[name] = opt.value.trim(); } } return true; }); } } const answers = {}; await forEachQuestion(async question => { let answer; if (args.silent === true) { if (!argDefined(argDefs, args, question.id) && !question.required) { return true; } answer = getDefault(question); if (argDefined(argDefs, args, question.id)) { answer = args[question.id]; const result = validate(question, answer); if (result !== true) { console.error("complete_install.js: Invalid valid for argument:", "--" + question.id); console.error("complete_install.js:", result); process.exit(1); } } answer = formatAnswer(question, answer); } else { answer = await ask(question, { config, strtcpsvr_config }); } if (question.id) { answers[question.id] = answer; } return args.silent === true ? true : answer; }); if (IBMi) { svrname = answers.strtcpsvr_svrname; autostart = answers.strtcpsvr_autostart; ccsid = answers.strtcpsvr_ccsid; nodePath = answers.strtcpsvr_nodePath; } // Create/update configuration file. if (fileExists(configPath)) { config.port = answers.port; if (answers.gitSupport === false) { config.gitSupport = false; } else if (config.hasOwnProperty("gitSupport")) { delete config.gitSupport; } if (IBMi) { if (answers.connectorLibrary) { config.connectorLibrary = answers.connectorLibrary; } else { if (config.hasOwnProperty("connectorLibrary")) { delete config.connectorLibrary; } if (config.hasOwnProperty("connectorIASP")) { delete config.connectorIASP; } } if (answers.connectorIASP && answers.connectorIASP !== "*SYSBAS") { config.connectorIASP = answers.connectorIASP; } else if (config.hasOwnProperty("connectorIASP")) { delete config.connectorIASP; } if (answers.puiInstance) { config.profounduiLibrary = answers.puiInstance.library; config.staticFilesDirectory = answers.puiInstance.docRoot; } else if (config.hasOwnProperty("profounduiLibrary")) { delete config.profounduiLibrary; } } fs.writeFileSync(configPath, stringifyConfig(config)); console.log(""); console.log("config.js updated."); console.log(""); } else { let content = fs.readFileSync(path.join(iutils.getSetupDir(), "config.js"), "utf8"); // eslint-disable-next-line no-eval eval("config = " + content.substring(content.indexOf("{"))); config.port = answers.port; if (answers.connectorLibrary) { config.connectorLibrary = answers.connectorLibrary; if (answers.connectorIASP !== "*SYSBAS") { config.connectorIASP = answers.connectorIASP; } } if (IBMi) { config.showIBMiParmDefn = true; if (answers.puiInstance) { config.profounduiLibrary = answers.puiInstance.library; config.staticFilesDirectory = answers.puiInstance.docRoot; } } if (answers.gitSupport === false) { config.gitSupport = false; } // Check if this is a workspace (if so, default to mysql) if (!IBMi && fileExists(path.join(deployDir, "modules", "app", ".noderun", "settings.json"))) { if (config.databaseConnections) delete config.databaseConnections; // remove property so that it's appended at the end when we set it config.databaseConnections = [{ name: "default", default: true, driver: "mysql", driverOptions: { user: "user-name", password: "your-password", host: "localhost", database: "database-name" } }]; } content = "\nmodule.exports = "; content += JSON.stringify(config, null, " "); content += "\n"; fs.writeFileSync(configPath, content); console.log(""); console.log("config.js created."); console.log(""); } installSamples = answers.installSamples == true; } else if (IBMi && args.silent === true && args.configure !== true && args.strtcpsvr_svrname) { if (validateServer(args.strtcpsvr_svrname) === true) { svrname = args.strtcpsvr_svrname; autostart = args.strtcpsvr_autostart; } } // Complete installation. process.stdin.destroy(); let setupArgs = []; if (args.silent === true) { setupArgs.push("--silent"); } if (IBMi) { if (config.connectorLibrary !== undefined) { setupArgs.push("--ibmi-connector-library"); } if (svrname) { setupArgs.push("--ibmi-instance=" + svrname); if (autostart) { setupArgs.push("--ibmi-instance-autostart"); } else { setupArgs.push("--no-ibmi-instance-autostart"); } if (ccsid) { setupArgs.push("--ibmi-instance-ccsid=" + ccsid); } if (nodePath) { setupArgs.push("--ibmi-instance-node-path=" + nodePath); } } } if (gitSupported && config.gitSupport !== false && nodeMajorVersion < 18) { setupArgs.push("--nodegit"); } if (installSamples || args.installSamples) { setupArgs.push("--installSamples"); } if (setupArgs.length === 0) { setupArgs = ""; } else { setupArgs = "-- " + setupArgs.join(" "); } try { child_process.execSync( `npm run setup ${setupArgs}`, { cwd: iutils.getPackageDir(), stdio: ["ignore", "inherit", "inherit"] } ); } catch { process.exit(1); } } catch (error) { process.stdin.destroy(); console.error(error); process.exit(1); } })(); async function ask(questionParm, configs, recursive) { let question = questionParm.prompt; let defaultAnswer = getDefault(questionParm, configs); if (questionParm.type === "boolean") { defaultAnswer = defaultAnswer === true ? "y" : "n"; } if (defaultAnswer == null) defaultAnswer = ""; if (typeof defaultAnswer !== "string") defaultAnswer = String(defaultAnswer); if (defaultAnswer !== "") { const lastChar = question.substr(question.length - 1, 1); if (lastChar === ":" || lastChar === "?") { question = question.substr(0, question.length - 1) + " (" + defaultAnswer + ")" + lastChar; } else { question = question + " (" + defaultAnswer + ")" + ":"; } } question += " "; const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); let answer = await new Promise(resolve => { readlineInterface.question(question, function(answer) { readlineInterface.close(); resolve(answer); }); }); if (answer == "") answer = defaultAnswer; answer = answer.trim(); const result = validate(questionParm, answer); if (result !== true) { console.log(result); answer = await ask(questionParm, configs, true); } if (recursive !== true) { answer = formatAnswer(questionParm, answer); } return answer; } function fileExists(file) { let exists = false; try { const stat = fs.statSync(file); if (stat && stat.isFile()) exists = true; } catch (err) { exists = false; } return exists; } function validateName(name) { const error = iutils.validateIBMiName(name); if (error) { return error; } return true; } function validateLibrary(name) { const err = validateName(name); if (err === true) { if (iutils.libraryExists(name)) return "Library " + name.toUpperCase() + " already exists. Please use a different name."; } else return err; return true; } function validateServer(name) { const err = validateName(name); if (err === true) { const exists = iutils.getIBMiInstances().some(el => el.name === name.toUpperCase()); if (exists) return "Server Instance " + name.toUpperCase() + " already exists. Please use a different name."; } else return err; return true; } function validateIASP(name) { const error = iutils.validateIBMiIASP(name); if (error) { return error; } return true; } function validatePort(port) { if (port === "0") return true; port = Number(port); if (!isNaN(port) && port >= 1 && port <= 65535) return true; return "Invalid port number. Valid range is 0-65535."; } function isValidNodePath(nodePath) { if (!fileExists(nodePath)) { return "Path " + nodePath + " does not exist. A valid path name must be entered."; } return true; } function isValidCcsid(ccsid) { const error = iutils.validateIBMiCCSID(parseInt(ccsid, 10)); if (error) { return error; } return true; } function validateYesNo(answer) { if (typeof answer === "boolean") return true; answer = answer.toUpperCase(); if (answer === "Y" || answer === "YES" || answer === "N" || answer === "NO") return true; return "Invalid answer. Use y or n."; } function stringifyConfig(config) { // JavaScript functions (like connectorIPFilter) will be lost when doing JSON.stringify, // so we need to do some magic to preserve them. const funkz = []; let configString = "\nmodule.exports = "; configString += JSON.stringify(config, (key, val) => { if (typeof val === "function") { const n = funkz.length; funkz.push(val.toString()); return `___function${n}___`; } return val; }, " "); configString += "\n"; for (let i = 0; i < funkz.length; i++) { configString = configString.replace(`"___function${i}___"`, funkz[i]); } return configString; } function validatePUIInstance(name) { name = name.toUpperCase(); const result = validateName(name); if (result !== true) { return result; } const data = getPUIInstanceData(name); if (!data.success) { return data.error; } return true; } function getPUIInstanceData(name) { // Read the instance DB record to determine the server root and config file paths. const instanceMember = `/QSYS.LIB/QUSRSYS.LIB/QATMHINSTC.FILE/${name}.MBR`; try { fs.accessSync(instanceMember, fs.constants.R_OK); } catch { return { success: false, error: `Instance ${name} does not exist.` }; } const record = child_process.execSync( `/usr/bin/rfile -rlQ 'QUSRSYS/QATMHINSTC(${name})'`, { shell: "/QOpenSys/usr/bin/qsh" } ).toString(); const match = record.match(/^\s*-apache\s+-d\s+([^\s]+)\s+-f\s+([^\s]+)/); if (!match) { return { success: false, error: `Instance record data in QUSRSYS/QATMHINSTC(${name}) is invalid.` }; } const serverRoot = match[1]; let confFile = match[2]; if (!path.isAbsolute(confFile)) { confFile = path.resolve(serverRoot, confFile); } try { fs.accessSync(confFile, fs.constants.R_OK); } catch { return { success: false, error: `Unable to read instance configuration file: ${confFile}.` }; } // Read the instance configuration file and split into lines. const conf = child_process.execSync( `/usr/bin/cat '${confFile}'`, { shell: "/QOpenSys/usr/bin/qsh" } ).toString(); const lines = conf.split("\n") .map(line => line.replace(/\r$/, "")) .filter(line => !line.includes("#")); // Determine PUI library. // First check for: Define PUI_LIBRARY let library = getPUIDefine("PUI_LIBRARY"); if (!library) { // Otherwise, look for ScriptAlias directives. // Users can disable various features by removing ScriptAlias, so we'll check against a list. const saPaths = [ "/profoundui/start", "/profoundui/auth/start", "/profoundui/genie", "/profoundui/auth/genie", "/profoundui/atrium", "/profoundui/atrium/menu" ]; for (const saPath of saPaths) { library = getScriptAliasLibrary(saPath); if (library) { break; } } } if (!library) { return { success: false, error: "Unable to read Profound UI library name from instance configuration." }; } // Determine DocumentRoot directory. let docRoot = getPUIDefine("DOCUMENT_ROOT_SOURCE"); if (!docRoot) { docRoot = getPUIDefine("DOCUMENT_ROOT"); } if (!docRoot) { docRoot = getDocumentRoot(); } if (!docRoot) { return { success: false, error: "Unable to read DocumentRoot directory from instance configuration." }; } if (!path.isAbsolute(docRoot)) { docRoot = path.resolve(serverRoot, docRoot); } // Replace PUI Developer name in doc root, if set. const developer = getPUIDefine("DEVELOPER"); if (developer) { docRoot = docRoot.replace(/\$\{DEVELOPER\}/g, developer); } return { success: true, library, docRoot }; // Gets a PUI variable define function getPUIDefine(name) { const re = new RegExp(`${name}\\s+(.+)`); for (const line of lines) { let match = line.match(/^\s*Define\s+(.+)/i); if (match) { const values = match[1]; match = values.match(re); if (match) { const value = match[1].trim(); return value !== "" ? value : undefined; } } } } // Gets library name from ScriptAlias directive matching alias path name. function getScriptAliasLibrary(alias) { const re = new RegExp(`^\\s*ScriptAlias\\s+${alias}\\s+(.+)`, "i"); for (const line of lines) { let match = line.match(re); if (match) { const value = match[1].trim(); match = value.match(/\/QSYS.LIB\/(.+)\.LIB/i); if (match) { const library = match[1].trim(); return library !== "" ? library.toUpperCase() : undefined; } } } } // Gets the value of the DocumentRoot directive. function getDocumentRoot() { for (const line of lines) { const match = line.match(/^\s*DocumentRoot\s+(.+)/i); if (match) { let value = match[1].trim(); if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { value = value.substring(1, value.length - 1); } return value !== "" ? value : undefined; } } } } function argDefined(argDefs, args, name) { // minimist sets unpassed boolean arguments to false. let defined = false; if (argDefs.boolean.includes(name)) { const found = process.argv.slice(2).find(arg => arg.startsWith("--" + name)); defined = found !== undefined; } else { defined = args[name] !== undefined; } return defined; } async function buildArgHelp() { const nameHeading = "Argument"; const labelHeading = "Description"; const dftHeading = "Default Value"; const opts = {}; let nameLen = nameHeading.length; let labelLen = labelHeading.length; const pad = 3; await forEachQuestion(question => { if (question.id) { const name = "--" + question.id; const label = question.label; opts[name] = { label, default: question.required ? getDefault(question) : "** NONE **" }; if (name.length > nameLen) { nameLen = name.length; } if (label.length > labelLen) { labelLen = label.length; } } return true; }); let argHelp = nameHeading.padEnd(nameLen + pad); argHelp += labelHeading.padEnd(labelLen + pad); argHelp += dftHeading; argHelp += "\n"; for (const name in opts) { argHelp += "\n"; argHelp += name.padEnd(nameLen + pad); argHelp += opts[name].label.padEnd(labelLen + pad); argHelp += opts[name].default; } return argHelp; } function checkItem(item) { const platform = IBMi ? "os400" : process.platform; const arch = process.arch; if (item.os && !checkPlatformArch(item.os, platform)) { return false; } if (item.cpu && !checkPlatformArch(item.cpu, arch)) { return false; } if (item.engines && item.engines.node && !semver.satisfies(process.versions.node, item.engines.node)) { return false; } return true; function checkPlatformArch(list, value) { if (!Array.isArray(list)) { list = [list]; } const allowList = list.filter(entry => !entry.startsWith("!")); const blockList = list.filter(entry => entry.startsWith("!")).map(entry => entry.substr(1)); if (blockList.includes(value)) { return false; } if (allowList.length > 0 && !allowList.includes(value)) { return false; } return true; } } function formatAnswer(question, answer) { if (question.id === "puiInstance") { answer = getPUIInstanceData(answer); } else if (question.type === "number") { answer = Number(answer); } else if (question.type === "boolean") { if (typeof answer === "string") { answer = answer.toUpperCase() === "Y" ? true : false; } } else if (question.type !== "node_path") { answer = answer = answer.toUpperCase(); } return answer; } function getDefault(question, configs) { if (configs == null) { configs = { config: {}, strtcpsvr_config: {} }; } const config = configs.config; const strtcpsvr_config = configs.strtcpsvr_config; let dft = question.type === "node_path" ? process.argv[0] : question.default; if (question.id) { let configValue; if (question.id.startsWith("strtcpsvr_")) { configValue = strtcpsvr_config[question.id.substring("strtcpsvr_".length)]; if (configValue !== undefined && question.type === "boolean") { configValue = configValue === "1" ? true : false; } } else { configValue = config[question.id]; } if (configValue !== undefined) { dft = configValue; } } return dft; } function getWarnings() { const warnings = []; for (const warning of install_info.warnings) { if (checkItem(warning)) { warnings.push(warning); } } return warnings; } async function forEachQuestion(fn) { await iter(install_info.questions); async function iter(questions) { for (const question of questions) { if (!checkItem(question)) { continue; } const ret = await fn(question); if (question.type === "boolean" && Array.isArray(question.questions) && ret === true) { await iter(question.questions); } } } } function validate(question, answer) { let validator; if (question.id === "port") { validator = validatePort; } else if (question.id === "strtcpsvr_ccsid") { validator = isValidCcsid; } else if (question.id === "puiInstance") { validator = validatePUIInstance; } else if (question.id === "connectorLibrary") { validator = validateLibrary; } else if (question.id === "connectorIASP") { validator = validateIASP; } else if (question.id === "strtcpsvr_svrname") { validator = validateServer; } else if (question.type === "boolean") { validator = validateYesNo; } else if (question.type === "node_path") { validator = isValidNodePath; } else if (question.type === "text" && question.textType === "ibmi_name") { validator = validateName; } if (typeof validator === "function") { const result = validator(answer); if (result !== true) { return result; } } return true; }