/*! * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ var externalAPI = require("./api"); var redNodes = require("./nodes"); var flows = require("./flows"); var storage = require("./storage"); var library = require("./library"); var plugins = require("./plugins"); var settings = require("./settings"); var express = require("express"); var path = require('path'); var fs = require("fs"); var os = require("os"); const {log,i18n,events,exec,util,hooks} = require("@node-red/util"); var runtimeMetricInterval = null; var started = false; var stubbedExpressApp = { get: function() {}, post: function() {}, put: function() {}, delete: function() {} } var adminApi = { auth: { needsPermission: function() {return function(req,res,next) {next()}} }, adminApp: stubbedExpressApp, server: {} } var nodeApp; var adminApp; var server; /** * Initialise the runtime module. * @param {Object} settings - the runtime settings object * @param {HTTPServer} server - the http server instance for the server to use * @param {AdminAPI} adminApi - an instance of @node-red/editor-api. TODO: This needs to be * better abstracted. * @memberof @node-red/runtime */ function init(userSettings,httpServer,_adminApi) { server = httpServer; if (server && server.on) { // Add a listener to the upgrade event so that we can properly timeout connection // attempts that do not get handled by any nodes in the user's flow. // See #2956 server.on('upgrade',(request, socket, head) => { // Add a no-op handler to the error event in case nothing upgrades this socket // before the remote end closes it. This ensures we don't get as uncaughtException socket.on("error", err => {}) setTimeout(function() { // If this request has been handled elsewhere, the upgrade will have // been completed and bytes written back to the client. // If nothing has been written on the socket, nothing has handled the // upgrade, so we can consider this an unhandled upgrade. if (socket.bytesWritten === 0) { socket.destroy(); } },userSettings.inboundWebSocketTimeout || 5000) }); } userSettings.version = getVersion(); settings.init(userSettings); nodeApp = express(); adminApp = express(); if (_adminApi) { adminApi = _adminApi; } redNodes.init(runtime); externalAPI.init(runtime); } var version; function getVersion() { if (!version) { version = require(path.join(__dirname,"..","package.json")).version; /* istanbul ignore else */ try { fs.statSync(path.join(__dirname,"..","..","..","..",".git")); version += "-git"; } catch(err) { // No git directory } } return version; } /** * Start the runtime. * @return {Promise} - resolves when the runtime is started. This does not mean the * flows will be running as they are started asynchronously. * @memberof @node-red/runtime */ function start() { return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json") .then(function() { return storage.init(runtime)}) .then(function() { return settings.load(storage)}) .then(function() { return library.init(runtime)}) .then(function() { if (log.metric()) { runtimeMetricInterval = setInterval(function() { reportMetrics(); }, settings.runtimeMetricInterval||15000); } log.info("\n\n"+log._("runtime.welcome")+"\n===================\n"); if (settings.version) { log.info(log._("runtime.version",{component:"Node-RED",version:"v"+settings.version})); } log.info(log._("runtime.version",{component:"Node.js ",version:process.version})); if (settings.UNSUPPORTED_VERSION) { log.error("*****************************************************************"); log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=8.9.0"})+" *"); log.error("*****************************************************************"); events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true}); } log.info(os.type()+" "+os.release()+" "+os.arch()+" "+os.endianness()); return redNodes.load().then(function() { let autoInstallModules = false; if (settings.hasOwnProperty('autoInstallModules')) { log.warn(log._("server.deprecatedOption",{old:"autoInstallModules", new:"externalModules.autoInstall"})); autoInstallModules = true; } if (settings.externalModules) { // autoInstallModules = autoInstall enabled && (no palette setting || palette install not disabled) autoInstallModules = settings.externalModules.autoInstall && (!settings.externalModules.palette || settings.externalModules.palette.allowInstall !== false) ; } var i; var nodeErrors = redNodes.getNodeList(function(n) { return n.err!=null;}); var nodeMissing = redNodes.getNodeList(function(n) { return n.module && n.enabled && !n.loaded && !n.err;}); if (nodeErrors.length > 0) { log.warn("------------------------------------------------------"); for (i=0;i 0) { log.warn(log._("server.missing-modules")); var missingModules = {}; for (i=0;i 0) { reinstallAttempts = 0; reinstallModules(installingModules); } } if (settings.settingsFile) { log.info(log._("runtime.paths.settings",{path:settings.settingsFile})); } if (settings.httpRoot !== undefined) { log.warn(log._("server.deprecatedOption",{old:"httpRoot", new: "httpNodeRoot/httpAdminRoot"})); } if (settings.readOnly){ log.info(log._("settings.readonly-mode")) } if (settings.httpStatic && settings.httpStatic.length) { for (let si = 0; si < settings.httpStatic.length; si++) { let p = path.resolve(settings.httpStatic[si].path); let r = settings.httpStatic[si].root || "/"; log.info(log._("runtime.paths.httpStatic",{path:`${p} > ${r}`})); } } return redNodes.loadContextsPlugin().then(function () { redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {}); started = true; }); }); }); } var reinstallAttempts = 0; var reinstallTimeout; function reinstallModules(moduleList) { const promises = []; const reinstallList = []; var installRetry = 30000; if (settings.hasOwnProperty('autoInstallModulesRetry')) { log.warn(log._("server.deprecatedOption",{old:"autoInstallModulesRetry", new:"externalModules.autoInstallRetry"})); installRetry = settings.autoInstallModulesRetry; } if (settings.externalModules && settings.externalModules.hasOwnProperty('autoInstallRetry')) { installRetry = settings.externalModules.autoInstallRetry * 1000; } for (var i=0;i { events.emit("runtime-event",{id:"node/added",retain:false,payload:m.nodes}); }).catch(err => { reinstallList.push(mod); })); })(moduleList[i]) } } Promise.all(promises).then(function(results) { if (reinstallList.length > 0) { reinstallAttempts++; // First 5 at 1x timeout, next 5 at 2x, next 5 at 4x, then 8x var timeout = installRetry * Math.pow(2,Math.min(Math.floor(reinstallAttempts/5),3)); reinstallTimeout = setTimeout(function() { reinstallModules(reinstallList); },timeout); } }); } function reportMetrics() { var memUsage = process.memoryUsage(); log.log({ level: log.METRIC, event: "runtime.memory.rss", value: memUsage.rss }); log.log({ level: log.METRIC, event: "runtime.memory.heapTotal", value: memUsage.heapTotal }); log.log({ level: log.METRIC, event: "runtime.memory.heapUsed", value: memUsage.heapUsed }); } /** * Stops the runtime. * * Once called, Node-RED should not be restarted until the Node.JS process is * restarted. * * @return {Promise} - resolves when the runtime is stopped. * @memberof @node-red/runtime */ function stop() { if (runtimeMetricInterval) { clearInterval(runtimeMetricInterval); runtimeMetricInterval = null; } if (reinstallTimeout) { clearTimeout(reinstallTimeout); } started = false; return redNodes.stopFlows().then(function(){ return redNodes.closeContextsPlugin(); }); } // This is the internal api var runtime = { version: getVersion, log: log, i18n: i18n, events: events, settings: settings, storage: storage, hooks: hooks, nodes: redNodes, plugins: plugins, flows: flows, library: library, exec: exec, util: util, get adminApi() { return adminApi }, get adminApp() { return adminApp }, get nodeApp() { return nodeApp }, get server() { return server }, isStarted: function() { return started; } }; /** * This module provides the core runtime component of Node-RED. * It does *not* include the Node-RED editor. All interaction with * this module is done using the api provided. * * @namespace @node-red/runtime */ module.exports = { init: init, start: start, stop: stop, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_comms */ comms: externalAPI.comms, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_flows */ flows: externalAPI.flows, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_library */ library: externalAPI.library, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_nodes */ nodes: externalAPI.nodes, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_settings */ settings: externalAPI.settings, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_projects */ projects: externalAPI.projects, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_context */ context: externalAPI.context, /** * @memberof @node-red/runtime * @mixes @node-red/runtime_plugins */ plugins: externalAPI.plugins, /** * Returns whether the runtime is started * @param {Object} opts * @param {User} opts.user - the user calling the api * @return {Promise} - whether the runtime is started * @function * @memberof @node-red/runtime */ isStarted: externalAPI.isStarted, /** * Returns version number of the runtime * @param {Object} opts * @param {User} opts.user - the user calling the api * @return {Promise} - the runtime version number * @function * @memberof @node-red/runtime */ version: externalAPI.version, /** * @memberof @node-red/diagnostics */ diagnostics:externalAPI.diagnostics, storage: storage, events: events, hooks: hooks, util: require("@node-red/util").util, get httpNode() { return nodeApp }, get httpAdmin() { return adminApp }, get server() { return server }, "_": runtime } /** * A user accessing the API * @typedef User * @type {object} */