UNPKG

iobroker.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

379 lines (378 loc) • 13.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __import_meta_url = typeof document === "undefined" ? new (require("url".replace("", ""))).URL("file:" + __filename).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href; var import_promisify_child_process = require("promisify-child-process"); var import_js_controller_common = require("@iobroker/js-controller-common"); var import_semver = require("semver"); var import_js_controller_cli = require("@iobroker/js-controller-cli"); var import_node_http = __toESM(require("node:http"), 1); var import_node_https = __toESM(require("node:https"), 1); var import_promises = require("node:timers/promises"); var import_fs_extra = __toESM(require("fs-extra"), 1); var import_node_url = __toESM(require("node:url"), 1); var import_node_process = __toESM(require("node:process"), 1); class UpgradeManager { /** Wait ms until controller is stopped */ STOP_TIMEOUT_MS = 5e3; /** Wait ms for delivery of final response */ SHUTDOWN_TIMEOUT = 1e4; /** Instance of admin to get information from */ adminInstance; /** Desired controller version */ version; /** Group id the process should run as */ gid; /** User id the process should run as */ uid; /** Response send by webserver */ response = { running: true, stderr: [], stdout: [] }; /** Used to stop the stop shutdown timeout */ shutdownAbortController; /** Logger to log to file and other transports */ logger; /** The server used for communicating upgrade status */ server; /** All socket connections of the webserver */ sockets = /* @__PURE__ */ new Set(); /** Name of the host for logging purposes */ hostname = import_js_controller_common.tools.getHostName(); constructor(args) { this.adminInstance = args.adminInstance; this.version = args.version; this.logger = this.setupLogger(); this.gid = args.gid; this.uid = args.uid; } /** * To prevent commands (including npm) running as root, we apply the passed in gid and uid */ applyUser() { if (!import_node_process.default.setuid || !import_node_process.default.setgid) { const errMessage = "Cannot ensure user and group ids on this system, because no POSIX platform"; this.log(errMessage, true); throw new Error(errMessage); } try { import_node_process.default.setgid(this.gid); import_node_process.default.setuid(this.uid); } catch (e) { const errMessage = `Could not ensure user and group ids on this system: ${e.message}`; this.log(errMessage, true); throw new Error(errMessage); } } /** * Set up the logger, to stream to file and other configured transports */ setupLogger() { const config = import_fs_extra.default.readJSONSync(import_js_controller_common.tools.getConfigFileName()); return (0, import_js_controller_common.logger)({ ...config.log, noStdout: false }); } /** * Parse the commands from the cli */ static parseCliCommands() { const additionalArgs = import_node_process.default.argv.slice(2); const version = additionalArgs[0]; const adminInstance = parseInt(additionalArgs[1]); const uid = parseInt(additionalArgs[2]); const gid = parseInt(additionalArgs[3]); const isValid = !!(0, import_semver.valid)(version); if (!isValid) { UpgradeManager.printUsage(); throw new Error("The provided version is not valid"); } if (isNaN(adminInstance)) { UpgradeManager.printUsage(); throw new Error("Please provide a valid admin instance"); } if (isNaN(uid)) { UpgradeManager.printUsage(); throw new Error("Please provide a valid uid"); } if (isNaN(gid)) { UpgradeManager.printUsage(); throw new Error("Please provide a valid gid"); } return { version, adminInstance, uid, gid }; } /** * Log via console and provide the logs for the server too * * @param message the message which will be logged * @param error if it is an error */ log(message, error = false) { if (error) { this.logger.error(`host.${this.hostname} [CONTROLLER_AUTO_UPGRADE] ${message}`); this.response.stderr.push(message); return; } this.logger.info(`host.${this.hostname} [CONTROLLER_AUTO_UPGRADE] ${message}`); this.response.stdout.push(message); } /** * Stops the js-controller via cli call */ async stopController() { if (import_js_controller_common.tools.isDocker()) { await (0, import_promisify_child_process.exec)("/opt/scripts/maintenance.sh on -kbn"); } else { await (0, import_promisify_child_process.exec)(`${import_js_controller_common.tools.appNameLowerCase} stop`); } await (0, import_promises.setTimeout)(this.STOP_TIMEOUT_MS); } /** * Starts the js-controller via cli */ startController() { if (import_js_controller_common.tools.isDocker()) { return (0, import_promisify_child_process.exec)("/opt/scripts/maintenance.sh off -y"); } return (0, import_promisify_child_process.exec)(`${import_js_controller_common.tools.appNameLowerCase} start`); } /** * Print how the module should be used */ static printUsage() { console.info('Example usage: "node upgradeManager.js <version> <adminInstance> <uid> <gid>"'); } /** * Install given version of js-controller */ async npmInstall() { const res = await import_js_controller_common.tools.installNodeModule(`iobroker.js-controller@${this.version}`, { cwd: "/opt/iobroker", debug: true }); this.response.stderr.push(...res.stderr.split("\n")); this.response.stdout.push(...res.stdout.split("\n")); this.response.success = res.success; if (!res.success) { throw new Error(`Could not install js-controller@${this.version}`); } } /** * Starts the web server for admin communication either secure or insecure * * @param params Web server configuration */ async startWebServer(params) { const { useHttps } = params; if (useHttps) { await this.startSecureWebServer(params); } else { await this.startInsecureWebServer(params); } } /** * Shuts down the server, restarts the controller and exits the program */ shutdownApp() { if (this.shutdownAbortController) { this.shutdownAbortController.abort(); } if (!this.server) { import_node_process.default.exit(); } this.destroySockets(); this.server.close(async () => { await this.startController(); this.log("Successfully started js-controller"); import_node_process.default.exit(); }); } /** * Destroy all sockets, to prevent requests from keeping server alive */ destroySockets() { for (const socket of this.sockets) { socket.destroy(); this.sockets.delete(socket); } } /** * This function is called when the webserver receives a message * * @param res server response */ webServerCallback(res) { res.writeHead(200, { "Access-Control-Allow-Origin": "*" }); res.end(JSON.stringify(this.response)); if (!this.response.running) { this.log("Final information delivered"); this.shutdownApp(); } } /** * Start an insecure web server for admin communication * * @param params Web server configuration */ async startInsecureWebServer(params) { const { port } = params; this.server = import_node_http.default.createServer((_req, res) => { this.webServerCallback(res); }); this.monitorSockets(this.server); await new Promise((resolve) => { this.server.listen(port, () => { resolve(); }); }); this.log(`Server is running on http://localhost:${port}`); } /** * Start a secure web server for admin communication * * @param params Web server configuration */ async startSecureWebServer(params) { const { port, certPublic, certPrivate } = params; this.server = import_node_https.default.createServer({ key: certPrivate, cert: certPublic }, (_req, res) => { this.webServerCallback(res); }); this.monitorSockets(this.server); await new Promise((resolve) => { this.server.listen(port, () => { resolve(); }); }); this.log(`Server is running on https://localhost:${port}`); } /** * Keep track of all existing sockets * * @param server the webserver */ monitorSockets(server) { server.on("connection", (socket) => { this.sockets.add(socket); server.once("close", () => { this.sockets.delete(socket); }); }); } /** * Get certificates from the DB * * @param params certificate information */ async getCertificates(params) { const { objects, certPublicName, certPrivateName } = params; const obj = await objects.getObjectAsync("system.certificates"); if (!obj) { throw new Error("No certificates found"); } const certs = obj.native.certificates; return { certPrivate: certs[certPrivateName], certPublic: certs[certPublicName] }; } /** * Collect parameters for webserver from admin instance */ async collectWebServerParameters() { const { objects } = await (0, import_js_controller_cli.dbConnectAsync)(false); const obj = await objects.getObjectAsync(`system.adapter.admin.${this.adminInstance}`); if (!obj) { UpgradeManager.printUsage(); throw new Error("Please provide a valid admin instance"); } if (obj.native.secure) { const { certPublic: certPublicName, certPrivate: certPrivateName } = obj.native; const { certPublic, certPrivate } = await this.getCertificates({ objects, certPublicName, certPrivateName }); return { useHttps: obj.native.secure, port: obj.native.port, certPublic, certPrivate }; } return { useHttps: false, port: obj.native.port }; } /** * Tells the upgrade manager, that server can be shut down on next response or on timeout */ async setFinished() { this.response.running = false; await this.startShutdownTimeout(); } /** * Start a timeout which starts controller and shuts down the app if expired */ async startShutdownTimeout() { this.shutdownAbortController = new AbortController(); try { await (0, import_promises.setTimeout)(this.SHUTDOWN_TIMEOUT, null, { signal: this.shutdownAbortController.signal }); this.log("Timeout expired, initializing shutdown"); this.shutdownApp(); } catch (e) { if (e.code !== "ABORT_ERR") { this.log(e.message, true); } } } } async function main() { const upgradeArguments = UpgradeManager.parseCliCommands(); const upgradeManager = new UpgradeManager(upgradeArguments); registerErrorHandlers(upgradeManager); const webServerParameters = await upgradeManager.collectWebServerParameters(); upgradeManager.log("Stopping controller"); await upgradeManager.stopController(); upgradeManager.log("Successfully stopped js-controller"); await upgradeManager.startWebServer(webServerParameters); upgradeManager.applyUser(); try { await upgradeManager.npmInstall(); } catch (e) { upgradeManager.log(e.message, true); } await upgradeManager.setFinished(); } function registerErrorHandlers(upgradeManager) { import_node_process.default.on("uncaughtException", (e) => { upgradeManager.log(`Uncaught Exception: ${e.stack}`, true); }); import_node_process.default.on("unhandledRejection", (rej) => { upgradeManager.log(`Unhandled rejection: ${rej instanceof Error ? rej.stack : JSON.stringify(rej)}`, true); }); } const modulePath = import_node_url.default.fileURLToPath(__import_meta_url || `file://${__filename}`); if (import_node_process.default.argv[1] === modulePath) { main(); } //# sourceMappingURL=upgradeManager.js.map