iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
379 lines (378 loc) • 13.1 kB
JavaScript
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
;