UNPKG

cloud-red

Version:

Serverless Node-RED for your cloud integration needs

473 lines (446 loc) 12.4 kB
/*! * 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 when = require('when'); var externalAPI = require('./api'); var redNodes = require('./nodes'); var storage = require('./storage'); var library = require('./library'); var events = require('./events'); var settings = require('./settings'); var exec = require('./exec'); var express = require('express'); var path = require('path'); var fs = require('fs'); var os = require('os'); const chalk = require('chalk'); var redUtil = require('../../util'); var log = redUtil.log; var i18n = redUtil.i18n; 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. <B>TODO</B>: This needs to be * better abstracted. * @memberof @node-red/runtime */ function init(userSettings, httpServer, _adminApi, __util) { server = httpServer; userSettings.version = getVersion(); settings.init(userSettings); nodeApp = express(); adminApp = express(); if (_adminApi) { adminApi = _adminApi; } redNodes.init(runtime); library.init(runtime); externalAPI.init(runtime); exec.init(runtime); if (__util) { log = __util.log; i18n = __util.i18n; } else { log = redUtil.log; i18n = redUtil.i18n; } } var version; function getVersion() { if (!version) { version = '0.5'; //TODO:(jteso) - i had to remove this --> 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() { if (log.metric()) { runtimeMetricInterval = setInterval(function() { reportMetrics(); }, settings.runtimeMetricInterval || 15000); } console.log( chalk.bold.white('\n\nWelcome to CloudRED\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: '>=4' }) + ' *' ); 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() { 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 < nodeErrors.length; i += 1) { if (nodeErrors[i].err.code === 'type_already_registered') { log.warn( '[' + nodeErrors[i].id + '] ' + log._('server.type-already-registered', { type: nodeErrors[i].err.details.type, module: nodeErrors[i].err.details.moduleA }) ); } else { log.warn('[' + nodeErrors[i].id + '] ' + nodeErrors[i].err); } } log.warn('------------------------------------------------------'); } if (nodeMissing.length > 0) { log.warn(log._('server.missing-modules')); var missingModules = {}; for (i = 0; i < nodeMissing.length; i++) { var missing = nodeMissing[i]; missingModules[missing.module] = missingModules[missing.module] || { module: missing.module, version: missing.pending_version || missing.version, types: [] }; missingModules[missing.module].types = missingModules[ missing.module ].types.concat(missing.types); } var moduleList = []; var promises = []; var installingModules = []; for (i in missingModules) { if (missingModules.hasOwnProperty(i)) { log.warn( ' - ' + i + ' (' + missingModules[i].version + '): ' + missingModules[i].types.join(', ') ); if (settings.autoInstallModules && i != 'node-red') { installingModules.push({ id: i, version: missingModules[i].version }); } } } if (!settings.autoInstallModules) { log.info(log._('server.removing-modules')); redNodes.cleanModuleList(); } else if (installingModules.length > 0) { reinstallAttempts = 0; reinstallModules(installingModules); } } if (settings.settingsFile) { log.info( log._('runtime.paths.settings', { path: settings.settingsFile }) ); } if (settings.httpStatic) { log.info( log._('runtime.paths.httpStatic', { path: path.resolve(settings.httpStatic) }) ); } return redNodes.loadContextsPlugin().then(function() { redNodes .loadFlows() .then(redNodes.startFlows) .catch(function(err) {}); started = true; }); }); }); } var reinstallAttempts; var reinstallTimeout; function reinstallModules(moduleList) { var promises = []; var failedModules = []; for (var i = 0; i < moduleList.length; i++) { if (settings.autoInstallModules && i != 'node-red') { promises.push( redNodes.installModule(moduleList[i].id, moduleList[i].version) ); } } when.settle(promises).then(function(results) { var reinstallList = []; for (var i = 0; i < results.length; i++) { if (results[i].state === 'rejected') { reinstallList.push(moduleList[i]); } else { events.emit('runtime-event', { id: 'node/added', retain: false, payload: results[i].value.nodes }); } } if (reinstallList.length > 0) { reinstallAttempts++; // First 5 at 1x timeout, next 5 at 2x, next 5 at 4x, then 8x var timeout = (settings.autoInstallModulesRetry || 30000) * 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. * @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, get log() { return log; }, get i18n() { return i18n; }, settings: settings, storage: storage, events: events, nodes: redNodes, library: library, exec: exec, util: require('../../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, /** * Returns whether the runtime is started * @param {Object} opts * @param {User} opts.user - the user calling the api * @return {Promise<Boolean>} - 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<String>} - the runtime version number * @function * @memberof @node-red/runtime */ version: externalAPI.version, storage: storage, events: events, util: require('../../util').util, get httpNode() { return nodeApp; }, get httpAdmin() { return adminApp; }, get server() { return server; }, _: runtime }; /** * A user accessing the API * @typedef User * @type {object} */