mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
4cb8e99430
Fixes #2956
411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
/*!
|
|
* 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 hooks = require("./hooks");
|
|
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} = 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. <B>TODO</B>: 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<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 (autoInstallModules && i != "node-red") {
|
|
installingModules.push({id:i,version:missingModules[i].version});
|
|
}
|
|
}
|
|
}
|
|
if (!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 = 0;
|
|
var reinstallTimeout;
|
|
function reinstallModules(moduleList) {
|
|
const promises = [];
|
|
const reinstallList = [];
|
|
const 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<moduleList.length;i++) {
|
|
if (moduleList[i].id != "node-red") {
|
|
(function(mod) {
|
|
promises.push(redNodes.installModule(mod.id,mod.version).then(m => {
|
|
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<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,
|
|
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}
|
|
*/
|