mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Add dynamic node api
Closes #322 - nodes modules can be installed/removed dynamically at runtime - nodes can be enabled/disabled - onpaletteadd/onpaletteremove api added to node definitions - initial implementation of nr-cli
This commit is contained in:
179
red/bin/nr-cli.js
Executable file
179
red/bin/nr-cli.js
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
;(function() {
|
||||
/**
|
||||
* Copyright 2014 IBM Corp.
|
||||
*
|
||||
* 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 request = require("request");
|
||||
var colors = require('colors');
|
||||
|
||||
|
||||
function formatBoolean(v,c) {
|
||||
if (v) {
|
||||
return ("["+c+"]");
|
||||
} else {
|
||||
return ("[ ]");
|
||||
}
|
||||
}
|
||||
|
||||
function formatNodeInfo(n) {
|
||||
var inError = n.hasOwnProperty("err");
|
||||
|
||||
var str = formatBoolean(n.enabled,"X")+formatBoolean(n.loaded,"L")+" ";
|
||||
str += n.id;
|
||||
if (n.enabled && n.loaded) {
|
||||
str = str.green;
|
||||
} else if (n.enabled && n.err) {
|
||||
str = str.red;
|
||||
} else {
|
||||
str = str.yellow;
|
||||
}
|
||||
if (n.module) {
|
||||
str += " ["+n.module+"]";
|
||||
}
|
||||
str += " "+n.types.join(", ");
|
||||
if (n.err) {
|
||||
str+=" "+n.err.red;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
var options;
|
||||
|
||||
if (process.argv[2] == "nodes") {
|
||||
|
||||
options = {
|
||||
url: 'http://localhost:1880/nodes',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
for (var i=0;i<info.length;i++) {
|
||||
var n = info[i];
|
||||
console.log(formatNodeInfo(n))
|
||||
}
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
} else if (process.argv[2] == "node" && process.argv[3]) {
|
||||
options = {
|
||||
url: 'http://localhost:1880/nodes/'+process.argv[3],
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
};
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
console.log(formatNodeInfo(info));
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
} else if (process.argv[2] == "enable-node" && process.argv[3]) {
|
||||
options = {
|
||||
method: "PUT",
|
||||
url: 'http://localhost:1880/nodes/'+process.argv[3],
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'content-type':'application/json'
|
||||
},
|
||||
body: JSON.stringify({enabled:true})
|
||||
};
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
console.log(formatNodeInfo(info));
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
} else if (process.argv[2] == "disable-node" && process.argv[3]) {
|
||||
options = {
|
||||
method: "PUT",
|
||||
url: 'http://localhost:1880/nodes/'+process.argv[3],
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'content-type':'application/json'
|
||||
},
|
||||
body: JSON.stringify({enabled:false})
|
||||
};
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
console.log(formatNodeInfo(info));
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
} else if (process.argv[2] == "install" && process.argv[3]) {
|
||||
options = {
|
||||
method: "POST",
|
||||
url: 'http://localhost:1880/nodes',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'content-type':'application/json'
|
||||
},
|
||||
body: JSON.stringify({module:process.argv[3]})
|
||||
};
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
for (var i=0;i<info.length;i++) {
|
||||
var n = info[i];
|
||||
console.log(formatNodeInfo(n))
|
||||
}
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
} else if (process.argv[2] == "remove" && process.argv[3]) {
|
||||
options = {
|
||||
method: "DELETE",
|
||||
url: 'http://localhost:1880/nodes/'+process.argv[3],
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
};
|
||||
request(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
var info = JSON.parse(body);
|
||||
for (var i=0;i<info.length;i++) {
|
||||
var n = info[i];
|
||||
console.log(formatNodeInfo(n))
|
||||
}
|
||||
} else if (error) {
|
||||
console.log(error.toString().red);
|
||||
} else {
|
||||
console.log((response.statusCode+": "+body).red);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
@@ -51,11 +51,10 @@ function init(_settings,storage) {
|
||||
registry.init(_settings);
|
||||
}
|
||||
|
||||
|
||||
function removeNode(info) {
|
||||
var nodeInfo = registry.getNodeInfo(info);
|
||||
function checkTypeInUse(id) {
|
||||
var nodeInfo = registry.getNodeInfo(id);
|
||||
if (!nodeInfo) {
|
||||
throw new Error("Unrecognised type/id: "+info);
|
||||
throw new Error("Unrecognised id: "+info);
|
||||
}
|
||||
var inUse = {};
|
||||
flows.each(function(n) {
|
||||
@@ -71,7 +70,25 @@ function removeNode(info) {
|
||||
var msg = nodesInUse.join(", ");
|
||||
throw new Error("Type in use: "+msg);
|
||||
}
|
||||
return registry.removeNode(nodeInfo.id);
|
||||
}
|
||||
|
||||
function removeNode(id) {
|
||||
checkTypeInUse(id);
|
||||
return registry.removeNode(id);
|
||||
}
|
||||
|
||||
function removeModule(module) {
|
||||
var info = registry.getNodeModuleInfo(module);
|
||||
for (var i=0;i<info.nodes.length;i++) {
|
||||
checkTypeInUse(info.nodes[i]);
|
||||
}
|
||||
return registry.removeModule(module);
|
||||
}
|
||||
|
||||
|
||||
function disableNode(id) {
|
||||
checkTypeInUse(id);
|
||||
return registry.disableNode(id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -86,9 +103,17 @@ module.exports = {
|
||||
addNode: registry.addNode,
|
||||
removeNode: removeNode,
|
||||
|
||||
addModule: registry.addModule,
|
||||
removeModule: removeModule,
|
||||
|
||||
enableNode: registry.enableNode,
|
||||
disableNode: disableNode,
|
||||
|
||||
// Node type registry
|
||||
registerType: registerType,
|
||||
getType: registry.get,
|
||||
getNodeInfo: registry.getNodeInfo,
|
||||
getNodeModuleInfo: registry.getNodeModuleInfo,
|
||||
getNodeList: registry.getNodeList,
|
||||
getNodeConfigs: registry.getNodeConfigs,
|
||||
getNodeConfig: registry.getNodeConfig,
|
||||
|
@@ -30,11 +30,17 @@ var settings;
|
||||
function filterNodeInfo(n) {
|
||||
var r = {
|
||||
id: n.id,
|
||||
types: n.types,
|
||||
name: n.name,
|
||||
types: n.types,
|
||||
enabled: n.enabled
|
||||
}
|
||||
if (n.err) {
|
||||
if (n.hasOwnProperty("loaded")) {
|
||||
r.loaded = n.loaded;
|
||||
}
|
||||
if (n.hasOwnProperty("module")) {
|
||||
r.module = n.module;
|
||||
}
|
||||
if (n.hasOwnProperty("err")) {
|
||||
r.err = n.err.toString();
|
||||
}
|
||||
return r;
|
||||
@@ -46,14 +52,52 @@ var registry = (function() {
|
||||
var nodeList = [];
|
||||
var nodeConstructors = {};
|
||||
var nodeTypeToId = {};
|
||||
var nodeModules = {};
|
||||
|
||||
function saveNodeList() {
|
||||
var nodeList = {};
|
||||
|
||||
for (var i in nodeConfigs) {
|
||||
if (nodeConfigs.hasOwnProperty(i)) {
|
||||
var nodeConfig = nodeConfigs[i];
|
||||
var n = filterNodeInfo(nodeConfig);
|
||||
n.file = nodeConfig.file;
|
||||
delete n.loaded;
|
||||
delete n.err;
|
||||
delete n.file;
|
||||
delete n.id;
|
||||
nodeList[i] = n;
|
||||
}
|
||||
}
|
||||
settings.set("nodes",nodeList);
|
||||
}
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
if (settings.available()) {
|
||||
nodeConfigs = settings.get("nodes")||{};
|
||||
} else {
|
||||
nodeConfigs = {};
|
||||
}
|
||||
nodeModules = {};
|
||||
nodeTypeToId = {};
|
||||
nodeConstructors = {};
|
||||
nodeList = [];
|
||||
nodeConfigCache = null;
|
||||
},
|
||||
|
||||
addNodeSet: function(id,set) {
|
||||
if (!set.err) {
|
||||
set.types.forEach(function(t) {
|
||||
nodeTypeToId[t] = id;
|
||||
});
|
||||
}
|
||||
|
||||
if (set.module) {
|
||||
nodeModules[set.module] = nodeModules[set.module]||{nodes:[]};
|
||||
nodeModules[set.module].nodes.push(id);
|
||||
}
|
||||
|
||||
nodeConfigs[id] = set;
|
||||
nodeList.push(id);
|
||||
nodeConfigCache = null;
|
||||
@@ -72,9 +116,27 @@ var registry = (function() {
|
||||
delete nodeConstructors[t];
|
||||
delete nodeTypeToId[t];
|
||||
});
|
||||
config.enabled = false;
|
||||
config.loaded = false;
|
||||
nodeConfigCache = null;
|
||||
return filterNodeInfo(config);
|
||||
},
|
||||
removeModule: function(module) {
|
||||
if (!settings.available()) {
|
||||
throw new Error("Settings unavailable");
|
||||
}
|
||||
var nodes = nodeModules[module];
|
||||
if (!nodes) {
|
||||
throw new Error("Unrecognised module: "+module);
|
||||
}
|
||||
var infoList = [];
|
||||
for (var i=0;i<nodes.nodes.length;i++) {
|
||||
infoList.push(registry.removeNode(nodes.nodes[i]));
|
||||
}
|
||||
delete nodeModules[module];
|
||||
saveNodeList();
|
||||
return infoList;
|
||||
},
|
||||
getNodeInfo: function(typeOrId) {
|
||||
if (nodeTypeToId[typeOrId]) {
|
||||
return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]);
|
||||
@@ -84,10 +146,13 @@ var registry = (function() {
|
||||
return null;
|
||||
},
|
||||
getNodeList: function() {
|
||||
return nodeList.map(function(id) {
|
||||
var n = nodeConfigs[id];
|
||||
return filterNodeInfo(n);
|
||||
});
|
||||
var list = [];
|
||||
for (var id in nodeConfigs) {
|
||||
if (nodeConfigs.hasOwnProperty(id)) {
|
||||
list.push(filterNodeInfo(nodeConfigs[id]))
|
||||
}
|
||||
}
|
||||
return list;
|
||||
},
|
||||
registerNodeConstructor: function(type,constructor) {
|
||||
if (nodeConstructors[type]) {
|
||||
@@ -112,7 +177,7 @@ var registry = (function() {
|
||||
var script = "";
|
||||
for (var i=0;i<nodeList.length;i++) {
|
||||
var config = nodeConfigs[nodeList[i]];
|
||||
if (config.enabled) {
|
||||
if (config.enabled && !config.err) {
|
||||
result += config.config;
|
||||
script += config.script;
|
||||
}
|
||||
@@ -131,7 +196,9 @@ var registry = (function() {
|
||||
var config = nodeConfigs[id];
|
||||
if (config) {
|
||||
var result = config.config;
|
||||
result += '<script type="text/javascript">'+config.script+'</script>';
|
||||
if (config.script) {
|
||||
result += '<script type="text/javascript">'+config.script+'</script>';
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
@@ -140,7 +207,7 @@ var registry = (function() {
|
||||
|
||||
getNodeConstructor: function(type) {
|
||||
var config = nodeConfigs[nodeTypeToId[type]];
|
||||
if (!config || config.enabled) {
|
||||
if (!config || (config.enabled && !config.err)) {
|
||||
return nodeConstructors[type];
|
||||
}
|
||||
return null;
|
||||
@@ -158,25 +225,47 @@ var registry = (function() {
|
||||
return nodeTypeToId[type];
|
||||
},
|
||||
|
||||
getModuleInfo: function(type) {
|
||||
return nodeModules[type];
|
||||
},
|
||||
|
||||
enableNodeSet: function(id) {
|
||||
if (!settings.available()) {
|
||||
throw new Error("Settings unavailable");
|
||||
}
|
||||
var config = nodeConfigs[id];
|
||||
if (config) {
|
||||
if (config.err) {
|
||||
throw new Error("cannot enable node with error");
|
||||
}
|
||||
delete config.err;
|
||||
config.enabled = true;
|
||||
if (!config.loaded) {
|
||||
// TODO: honour the promise this returns
|
||||
loadNodeModule(config);
|
||||
}
|
||||
nodeConfigCache = null;
|
||||
saveNodeList();
|
||||
} else {
|
||||
throw new Error("Unrecognised id: "+id);
|
||||
}
|
||||
return filterNodeInfo(config);
|
||||
},
|
||||
|
||||
disableNodeSet: function(id) {
|
||||
if (!settings.available()) {
|
||||
throw new Error("Settings unavailable");
|
||||
}
|
||||
var config = nodeConfigs[id];
|
||||
if (config) {
|
||||
// TODO: persist setting
|
||||
config.enabled = false;
|
||||
nodeConfigCache = null;
|
||||
saveNodeList();
|
||||
} else {
|
||||
throw new Error("Unrecognised id: "+id);
|
||||
}
|
||||
|
||||
}
|
||||
return filterNodeInfo(config);
|
||||
},
|
||||
|
||||
saveNodeList: saveNodeList
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -185,6 +274,7 @@ var registry = (function() {
|
||||
function init(_settings) {
|
||||
Node = require("./Node");
|
||||
settings = _settings;
|
||||
registry.init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,22 +334,28 @@ function scanTreeForNodesModules(moduleName) {
|
||||
var pm = path.join(dir,"node_modules");
|
||||
try {
|
||||
var files = fs.readdirSync(pm);
|
||||
files.forEach(function(fn) {
|
||||
if (!moduleName || fn == moduleName) {
|
||||
var pkgfn = path.join(pm,fn,"package.json");
|
||||
try {
|
||||
var pkg = require(pkgfn);
|
||||
if (pkg['node-red']) {
|
||||
var moduleDir = path.join(pm,fn);
|
||||
results.push({dir:moduleDir,package:pkg});
|
||||
for (var i=0;i<files.length;i++) {
|
||||
var fn = files[i];
|
||||
if (!registry.getModuleInfo(fn)) {
|
||||
if (!moduleName || fn == moduleName) {
|
||||
var pkgfn = path.join(pm,fn,"package.json");
|
||||
try {
|
||||
var pkg = require(pkgfn);
|
||||
if (pkg['node-red']) {
|
||||
var moduleDir = path.join(pm,fn);
|
||||
results.push({dir:moduleDir,package:pkg});
|
||||
}
|
||||
} catch(err) {
|
||||
if (err.code != "MODULE_NOT_FOUND") {
|
||||
// TODO: handle unexpected error
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if (err.code != "MODULE_NOT_FOUND") {
|
||||
// TODO: handle unexpected error
|
||||
if (fn == moduleName) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(err) {
|
||||
}
|
||||
|
||||
@@ -313,15 +409,23 @@ function loadNodesFromModule(moduleDir,pkg) {
|
||||
function loadNodeConfig(file,module,name) {
|
||||
var id = crypto.createHash('sha1').update(file).digest("hex");
|
||||
|
||||
if (registry.getNodeInfo(id)) {
|
||||
throw new Error(file+" already loaded");
|
||||
var info = registry.getNodeInfo(id);
|
||||
|
||||
var isEnabled = true;
|
||||
|
||||
if (info) {
|
||||
if (info.hasOwnProperty("loaded")) {
|
||||
throw new Error(file+" already loaded");
|
||||
}
|
||||
isEnabled = info.enabled;
|
||||
}
|
||||
|
||||
var node = {
|
||||
id: id,
|
||||
file: file,
|
||||
template: file.replace(/\.js$/,".html"),
|
||||
enabled: true
|
||||
enabled: isEnabled,
|
||||
loaded:false
|
||||
}
|
||||
|
||||
if (module) {
|
||||
@@ -411,7 +515,9 @@ function load(defaultNodesDir,disableNodePathScan) {
|
||||
when.settle(promises).then(function(results) {
|
||||
// Trigger a load of the configs to get it precached
|
||||
registry.getAllNodeConfigs();
|
||||
|
||||
if (settings.available()) {
|
||||
registry.saveNodeList();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -428,6 +534,9 @@ function load(defaultNodesDir,disableNodePathScan) {
|
||||
function loadNodeModule(node) {
|
||||
var nodeDir = path.dirname(node.file);
|
||||
var nodeFn = path.basename(node.file);
|
||||
if (!node.enabled) {
|
||||
return when.resolve(node);
|
||||
}
|
||||
try {
|
||||
var loadPromise = null;
|
||||
var r = require(node.file);
|
||||
@@ -436,57 +545,74 @@ function loadNodeModule(node) {
|
||||
if (promise != null && typeof promise.then === "function") {
|
||||
loadPromise = promise.then(function() {
|
||||
node.enabled = true;
|
||||
node.loaded = true;
|
||||
return node;
|
||||
}).otherwise(function(err) {
|
||||
node.err = err;
|
||||
node.enabled = false;
|
||||
return node;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (loadPromise == null) {
|
||||
node.enabled = true;
|
||||
node.loaded = true;
|
||||
loadPromise = when.resolve(node);
|
||||
}
|
||||
return loadPromise;
|
||||
} catch(err) {
|
||||
node.err = err;
|
||||
node.enabled = false;
|
||||
return when.resolve(node);
|
||||
}
|
||||
}
|
||||
|
||||
function addNode(options) {
|
||||
var nodes = [];
|
||||
if (options.file) {
|
||||
try {
|
||||
nodes.push(loadNodeConfig(options.file));
|
||||
} catch(err) {
|
||||
return when.reject(err);
|
||||
}
|
||||
} else if (options.module) {
|
||||
var moduleFiles = scanTreeForNodesModules(options.module);
|
||||
if (moduleFiles.length === 0) {
|
||||
var err = new Error("Cannot find module '" + options.module + "'");
|
||||
err.code = 'MODULE_NOT_FOUND';
|
||||
return when.reject(err);
|
||||
}
|
||||
moduleFiles.forEach(function(moduleFile) {
|
||||
nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
|
||||
});
|
||||
}
|
||||
function loadNodeList(nodes) {
|
||||
var promises = [];
|
||||
nodes.forEach(function(node) {
|
||||
promises.push(loadNodeModule(node));
|
||||
});
|
||||
|
||||
return when.settle(promises).then(function(results) {
|
||||
return results.map(function(r) {
|
||||
registry.saveNodeList();
|
||||
var list = results.map(function(r) {
|
||||
return filterNodeInfo(r.value);
|
||||
});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
function addNode(file) {
|
||||
if (!settings.available()) {
|
||||
throw new Error("Settings unavailable");
|
||||
}
|
||||
var nodes = [];
|
||||
try {
|
||||
nodes.push(loadNodeConfig(file));
|
||||
} catch(err) {
|
||||
return when.reject(err);
|
||||
}
|
||||
return loadNodeList(nodes);
|
||||
}
|
||||
|
||||
function addModule(module) {
|
||||
if (!settings.available()) {
|
||||
throw new Error("Settings unavailable");
|
||||
}
|
||||
var nodes = [];
|
||||
if (registry.getModuleInfo(module)) {
|
||||
return when.reject(new Error("Module already loaded"));
|
||||
}
|
||||
var moduleFiles = scanTreeForNodesModules(module);
|
||||
if (moduleFiles.length === 0) {
|
||||
var err = new Error("Cannot find module '" + module + "'");
|
||||
err.code = 'MODULE_NOT_FOUND';
|
||||
return when.reject(err);
|
||||
}
|
||||
moduleFiles.forEach(function(moduleFile) {
|
||||
nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
|
||||
});
|
||||
return loadNodeList(nodes);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init:init,
|
||||
load:load,
|
||||
@@ -494,11 +620,15 @@ module.exports = {
|
||||
registerType: registry.registerNodeConstructor,
|
||||
get: registry.getNodeConstructor,
|
||||
getNodeInfo: registry.getNodeInfo,
|
||||
getNodeModuleInfo: registry.getModuleInfo,
|
||||
getNodeList: registry.getNodeList,
|
||||
getNodeConfigs: registry.getAllNodeConfigs,
|
||||
getNodeConfig: registry.getNodeConfig,
|
||||
addNode: addNode,
|
||||
removeNode: registry.removeNode,
|
||||
enableNode: registry.enableNodeSet,
|
||||
disableNode: registry.disableNodeSet
|
||||
disableNode: registry.disableNodeSet,
|
||||
|
||||
addModule: addModule,
|
||||
removeModule: registry.removeModule
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ var comms = require("./comms");
|
||||
var log = require("./log");
|
||||
var util = require("./util");
|
||||
var fs = require("fs");
|
||||
var settings = null;
|
||||
var settings = require("./settings");
|
||||
var credentials = require("./nodes/credentials");
|
||||
|
||||
var path = require('path');
|
||||
@@ -33,9 +33,8 @@ var events = require("events");
|
||||
var RED = {
|
||||
|
||||
init: function(httpServer,userSettings) {
|
||||
settings = userSettings;
|
||||
settings.version = this.version();
|
||||
|
||||
userSettings.version = this.version();
|
||||
settings.init(userSettings);
|
||||
server.init(httpServer,settings);
|
||||
library.init();
|
||||
return server.app;
|
||||
@@ -49,6 +48,7 @@ var RED = {
|
||||
events: events,
|
||||
log: log,
|
||||
comms: comms,
|
||||
settings:settings,
|
||||
util: util,
|
||||
version: function () {
|
||||
var p = require(path.join(process.env.NODE_RED_HOME,"package.json"));
|
||||
@@ -64,6 +64,5 @@ RED.__defineGetter__("app", function() { console.log("Deprecated use of RED.app
|
||||
RED.__defineGetter__("httpAdmin", function() { return server.app });
|
||||
RED.__defineGetter__("httpNode", function() { return server.nodeApp });
|
||||
RED.__defineGetter__("server", function() { return server.server });
|
||||
RED.__defineGetter__("settings", function() { return settings });
|
||||
|
||||
module.exports = RED;
|
||||
|
283
red/server.js
283
red/server.js
@@ -17,6 +17,7 @@
|
||||
var express = require('express');
|
||||
var util = require('util');
|
||||
var when = require('when');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var createUI = require("./ui");
|
||||
var redNodes = require("./nodes");
|
||||
@@ -36,10 +37,6 @@ function createServer(_server,_settings) {
|
||||
app = createUI(settings);
|
||||
nodeApp = express();
|
||||
|
||||
app.get("/nodes",function(req,res) {
|
||||
res.send(redNodes.getNodeConfigs());
|
||||
});
|
||||
|
||||
app.get("/flows",function(req,res) {
|
||||
res.json(redNodes.getFlows());
|
||||
});
|
||||
@@ -60,43 +57,82 @@ function createServer(_server,_settings) {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
app.get("/nodes",function(req,res) {
|
||||
if (req.get("accept") == "application/json") {
|
||||
res.json(redNodes.getNodeList());
|
||||
} else {
|
||||
res.send(redNodes.getNodeConfigs());
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/nodes",
|
||||
express.json(),
|
||||
function(req,res) {
|
||||
if (!settings.available()) {
|
||||
res.send(400,new Error("Settings unavailable").toString());
|
||||
return;
|
||||
}
|
||||
var node = req.body;
|
||||
if (!node.file && !node.module) {
|
||||
var promise;
|
||||
if (node.file) {
|
||||
promise = redNodes.addNode(node.file).then(reportAddedModules);
|
||||
} else if (node.module) {
|
||||
var module = redNodes.getNodeModuleInfo(node.module);
|
||||
if (module) {
|
||||
res.send(400,"Module already loaded");
|
||||
return;
|
||||
}
|
||||
promise = installModule(node.module);
|
||||
} else {
|
||||
res.send(400,"Invalid request");
|
||||
return;
|
||||
}
|
||||
redNodes.addNode(node).then(function(info) {
|
||||
comms.publish("node/added",info,false);
|
||||
util.log("[red] Added node types:");
|
||||
for (var j=0;j<info.length;j++) {
|
||||
for (var i=0;i<info[j].types.length;i++) {
|
||||
util.log("[red] - "+info[j].types[i]);
|
||||
}
|
||||
}
|
||||
promise.then(function(info) {
|
||||
res.json(info);
|
||||
}).otherwise(function(err) {
|
||||
res.send(400,err.toString());
|
||||
if (err.code === 404) {
|
||||
res.send(404);
|
||||
} else {
|
||||
res.send(400,err.toString());
|
||||
}
|
||||
});
|
||||
},
|
||||
function(err,req,res,next) {
|
||||
console.log(err.toString());
|
||||
res.send(400,err);
|
||||
}
|
||||
);
|
||||
|
||||
app.delete("/nodes/:id",
|
||||
function(req,res) {
|
||||
if (!settings.available()) {
|
||||
res.send(400,new Error("Settings unavailable").toString());
|
||||
return;
|
||||
}
|
||||
var id = req.params.id;
|
||||
var removedNodes = [];
|
||||
try {
|
||||
var info = redNodes.removeNode(id);
|
||||
comms.publish("node/removed",info,false);
|
||||
util.log("[red] Removed node types:");
|
||||
for (var i=0;i<info.types.length;i++) {
|
||||
util.log("[red] - "+info.types[i]);
|
||||
var node = redNodes.getNodeInfo(id);
|
||||
var promise = null;
|
||||
if (!node) {
|
||||
var module = redNodes.getNodeModuleInfo(id);
|
||||
if (!module) {
|
||||
res.send(404);
|
||||
return;
|
||||
} else {
|
||||
promise = uninstallModule(id);
|
||||
}
|
||||
} else {
|
||||
promise = when.resolve([redNodes.removeNode(id)]).then(reportRemovedModules);
|
||||
}
|
||||
res.json(info);
|
||||
|
||||
promise.then(function(removedNodes) {
|
||||
res.json(removedNodes);
|
||||
}).otherwise(function(err) {
|
||||
console.log(err.stack);
|
||||
res.send(400,err.toString());
|
||||
});
|
||||
} catch(err) {
|
||||
res.send(400,err.toString());
|
||||
}
|
||||
@@ -108,46 +144,199 @@ function createServer(_server,_settings) {
|
||||
|
||||
app.get("/nodes/:id", function(req,res) {
|
||||
var id = req.params.id;
|
||||
var config = redNodes.getNodeConfig(id);
|
||||
if (config) {
|
||||
res.send(config);
|
||||
var result = null;
|
||||
if (req.get("accept") == "application/json") {
|
||||
result = redNodes.getNodeInfo(id);
|
||||
} else {
|
||||
result = redNodes.getNodeConfig(id);
|
||||
}
|
||||
if (result) {
|
||||
res.send(result);
|
||||
} else {
|
||||
res.send(404);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/nodes/:id",
|
||||
express.json(),
|
||||
function(req,res) {
|
||||
if (!settings.available()) {
|
||||
res.send(400,new Error("Settings unavailable").toString());
|
||||
return;
|
||||
}
|
||||
var body = req.body;
|
||||
if (!body.hasOwnProperty("enabled")) {
|
||||
res.send(400,"Invalid request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var info;
|
||||
var id = req.params.id;
|
||||
var node = redNodes.getNodeInfo(id);
|
||||
if (!node) {
|
||||
res.send(404);
|
||||
} else if (!node.err && node.enabled === body.enabled) {
|
||||
res.json(node);
|
||||
} else {
|
||||
if (body.enabled) {
|
||||
info = redNodes.enableNode(id);
|
||||
} else {
|
||||
info = redNodes.disableNode(id);
|
||||
}
|
||||
if (info.enabled == body.enabled && !info.err) {
|
||||
comms.publish("node/"+(body.enabled?"enabled":"disabled"),info,false);
|
||||
util.log("[red] "+(body.enabled?"Enabled":"Disabled")+" node types:");
|
||||
for (var i=0;i<info.types.length;i++) {
|
||||
util.log("[red] - "+info.types[i]);
|
||||
}
|
||||
} else if (body.enabled && info.err) {
|
||||
util.log("[red] Failed to enable node:");
|
||||
util.log("[red] - "+info.name+" : "+info.err);
|
||||
}
|
||||
res.json(info);
|
||||
}
|
||||
} catch(err) {
|
||||
res.send(400,err.toString());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
function reportAddedModules(info) {
|
||||
comms.publish("node/added",info,false);
|
||||
if (info.length > 0) {
|
||||
util.log("[red] Added node types:");
|
||||
for (var i=0;i<info.length;i++) {
|
||||
for (var j=0;j<info[i].types.length;j++) {
|
||||
util.log("[red] - "+
|
||||
(info[i].module?info[i].module+":":"")+
|
||||
info[i].types[j]+
|
||||
(info[i].err?" : "+info[i].err:"")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function reportRemovedModules(removedNodes) {
|
||||
comms.publish("node/removed",removedNodes,false);
|
||||
util.log("[red] Removed node types:");
|
||||
for (var j=0;j<removedNodes.length;j++) {
|
||||
for (var i=0;i<removedNodes[j].types.length;i++) {
|
||||
util.log("[red] - "+(removedNodes[i].module?removedNodes[i].module+":":"")+removedNodes[j].types[i]);
|
||||
}
|
||||
}
|
||||
return removedNodes;
|
||||
}
|
||||
|
||||
function installModule(module) {
|
||||
//TODO: ensure module is 'safe'
|
||||
return when.promise(function(resolve,reject) {
|
||||
if (/[\s;]/.test(module)) {
|
||||
reject(new Error("Invalid module name"));
|
||||
return;
|
||||
}
|
||||
util.log("[red] Installing module: "+module);
|
||||
var child = exec('npm install --production '+module, function(err, stdin, stdout) {
|
||||
if (err) {
|
||||
var lookFor404 = new RegExp(" 404 .*"+module+"$","m");
|
||||
if (lookFor404.test(stdout)) {
|
||||
util.log("[red] Installation of module "+module+" failed: not found");
|
||||
var e = new Error();
|
||||
e.code = 404;
|
||||
reject(e);
|
||||
} else {
|
||||
util.log("[red] Installation of module "+module+" failed:");
|
||||
util.log("------------------------------------------");
|
||||
console.log(err.toString());
|
||||
util.log("------------------------------------------");
|
||||
reject(new Error("Install failed"));
|
||||
}
|
||||
} else {
|
||||
util.log("[red] Installed module: "+module);
|
||||
resolve(redNodes.addModule(module).then(reportAddedModules));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallModule(module) {
|
||||
var list = redNodes.removeModule(module);
|
||||
return when.promise(function(resolve,reject) {
|
||||
if (/[\s;]/.test(module)) {
|
||||
reject(new Error("Invalid module name"));
|
||||
return;
|
||||
}
|
||||
util.log("[red] Removing module: "+module);
|
||||
var child = exec('npm remove '+module, function(err, stdin, stdout) {
|
||||
if (err) {
|
||||
util.log("[red] Removal of module "+module+" failed:");
|
||||
util.log("------------------------------------------");
|
||||
console.log(err.toString());
|
||||
util.log("------------------------------------------");
|
||||
reject(new Error("Removal failed"));
|
||||
} else {
|
||||
util.log("[red] Removed module: "+module);
|
||||
reportRemovedModules(list);
|
||||
resolve(list);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
var defer = when.defer();
|
||||
|
||||
storage.init(settings).then(function() {
|
||||
console.log("\nWelcome to Node-RED\n===================\n");
|
||||
if (settings.version) {
|
||||
util.log("[red] Version: "+settings.version);
|
||||
}
|
||||
util.log("[red] Loading palette nodes");
|
||||
redNodes.init(settings,storage);
|
||||
redNodes.load().then(function() {
|
||||
var nodes = redNodes.getNodeList();
|
||||
var nodeErrors = nodes.filter(function(n) { return n.err!=null;});
|
||||
if (nodeErrors.length > 0) {
|
||||
util.log("------------------------------------------");
|
||||
if (settings.verbose) {
|
||||
for (var i=0;i<nodeErrors.length;i+=1) {
|
||||
util.log("["+nodeErrors[i].name+"] "+nodeErrors[i].err);
|
||||
}
|
||||
} else {
|
||||
util.log("[red] Failed to register "+nodeErrors.length+" node type"+(nodeErrors.length==1?"":"s"));
|
||||
util.log("[red] Run with -v for details");
|
||||
}
|
||||
util.log("------------------------------------------");
|
||||
settings.load(storage).then(function() {
|
||||
console.log("\nWelcome to Node-RED\n===================\n");
|
||||
if (settings.version) {
|
||||
util.log("[red] Version: "+settings.version);
|
||||
}
|
||||
defer.resolve();
|
||||
|
||||
redNodes.loadFlows();
|
||||
util.log("[red] Loading palette nodes");
|
||||
redNodes.init(settings,storage);
|
||||
redNodes.load().then(function() {
|
||||
var i;
|
||||
var nodes = redNodes.getNodeList();
|
||||
var nodeErrors = nodes.filter(function(n) { return n.err!=null;});
|
||||
var nodeMissing = nodes.filter(function(n) { return n.module && n.enabled && !n.loaded && !n.err;});
|
||||
if (nodeErrors.length > 0) {
|
||||
util.log("------------------------------------------");
|
||||
if (settings.verbose) {
|
||||
for (i=0;i<nodeErrors.length;i+=1) {
|
||||
util.log("["+nodeErrors[i].name+"] "+nodeErrors[i].err);
|
||||
}
|
||||
} else {
|
||||
util.log("[red] Failed to register "+nodeErrors.length+" node type"+(nodeErrors.length==1?"":"s"));
|
||||
util.log("[red] Run with -v for details");
|
||||
}
|
||||
util.log("------------------------------------------");
|
||||
}
|
||||
if (nodeMissing.length > 0) {
|
||||
util.log("[red] Missing node modules:");
|
||||
var missingModules = {};
|
||||
for (i=0;i<nodeMissing.length;i++) {
|
||||
var missing = nodeMissing[i];
|
||||
missingModules[missing.module] = (missingModules[missing.module]||[]).concat(missing.types);
|
||||
}
|
||||
var promises = [];
|
||||
for (i in missingModules) {
|
||||
if (missingModules.hasOwnProperty(i)) {
|
||||
util.log(" - "+i+": "+missingModules[i].join(", "));
|
||||
promises.push(installModule(i));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
defer.resolve();
|
||||
|
||||
redNodes.loadFlows();
|
||||
}).otherwise(function(err) {
|
||||
console.log(err);
|
||||
});
|
||||
comms.start();
|
||||
});
|
||||
comms.start();
|
||||
}).otherwise(function(err) {
|
||||
defer.reject(err);
|
||||
});
|
||||
|
70
red/settings.js
Normal file
70
red/settings.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright 2014 IBM Corp.
|
||||
*
|
||||
* 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 userSettings = null;
|
||||
var globalSettings = null;
|
||||
var storage = null;
|
||||
|
||||
var persistentSettings = {
|
||||
init: function(settings) {
|
||||
userSettings = settings;
|
||||
|
||||
for (var i in settings) {
|
||||
if (settings.hasOwnProperty(i)) {
|
||||
(function() {
|
||||
var j = i;
|
||||
persistentSettings.__defineGetter__(j,function() { return userSettings[j]; });
|
||||
persistentSettings.__defineSetter__(j,function() { throw new Error("Property '"+i+"' is read-only"); });
|
||||
})();
|
||||
}
|
||||
}
|
||||
globalSettings = null;
|
||||
},
|
||||
load: function(_storage) {
|
||||
storage = _storage;
|
||||
return storage.getSettings().then(function(_settings) {
|
||||
globalSettings = _settings;
|
||||
});
|
||||
},
|
||||
get: function(prop) {
|
||||
if (userSettings.hasOwnProperty(prop)) {
|
||||
return userSettings[prop];
|
||||
}
|
||||
if (globalSettings === null) {
|
||||
throw new Error("Settings not available");
|
||||
}
|
||||
return globalSettings[prop];
|
||||
},
|
||||
|
||||
set: function(prop,value) {
|
||||
if (userSettings.hasOwnProperty(prop)) {
|
||||
throw new Error("Property '"+prop+"' is read-only");
|
||||
}
|
||||
if (globalSettings === null) {
|
||||
throw new Error("Settings not available");
|
||||
}
|
||||
globalSettings[prop] = value;
|
||||
return storage.saveSettings(globalSettings);
|
||||
},
|
||||
|
||||
available: function() {
|
||||
return (globalSettings !== null);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = persistentSettings;
|
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2013 IBM Corp.
|
||||
* Copyright 2013, 2014 IBM Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
var when = require('when');
|
||||
|
||||
var storageModule;
|
||||
var settingsAvailable;
|
||||
|
||||
function moduleSelector(aSettings) {
|
||||
var toReturn;
|
||||
@@ -38,48 +39,64 @@ function is_malicious(path) {
|
||||
}
|
||||
|
||||
var storageModuleInterface = {
|
||||
init : function(settings) {
|
||||
init: function(settings) {
|
||||
try {
|
||||
storageModule = moduleSelector(settings);
|
||||
settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings");
|
||||
} catch (e) {
|
||||
return when.reject(e);
|
||||
}
|
||||
return storageModule.init(settings);
|
||||
},
|
||||
getFlows : function() {
|
||||
getFlows: function() {
|
||||
return storageModule.getFlows();
|
||||
},
|
||||
saveFlows : function(flows) {
|
||||
saveFlows: function(flows) {
|
||||
return storageModule.saveFlows(flows);
|
||||
},
|
||||
getCredentials : function() {
|
||||
getCredentials: function() {
|
||||
return storageModule.getCredentials();
|
||||
},
|
||||
saveCredentials : function(credentials) {
|
||||
saveCredentials: function(credentials) {
|
||||
return storageModule.saveCredentials(credentials);
|
||||
},
|
||||
getAllFlows : function() {
|
||||
getSettings: function() {
|
||||
if (settingsAvailable) {
|
||||
return storageModule.getSettings();
|
||||
} else {
|
||||
return when.resolve(null);
|
||||
}
|
||||
},
|
||||
saveSettings: function(settings) {
|
||||
if (settingsAvailable) {
|
||||
return storageModule.saveSettings(settings);
|
||||
} else {
|
||||
return when.resolve();
|
||||
}
|
||||
},
|
||||
/* Library Functions */
|
||||
getAllFlows: function() {
|
||||
return storageModule.getAllFlows();
|
||||
},
|
||||
getFlow : function(fn) {
|
||||
getFlow: function(fn) {
|
||||
if (is_malicious(fn)) {
|
||||
return when.reject(new Error('forbidden flow name'));
|
||||
}
|
||||
return storageModule.getFlow(fn);
|
||||
},
|
||||
saveFlow : function(fn, data) {
|
||||
saveFlow: function(fn, data) {
|
||||
if (is_malicious(fn)) {
|
||||
return when.reject(new Error('forbidden flow name'));
|
||||
}
|
||||
return storageModule.saveFlow(fn, data);
|
||||
},
|
||||
getLibraryEntry : function(type, path) {
|
||||
getLibraryEntry: function(type, path) {
|
||||
if (is_malicious(path)) {
|
||||
return when.reject(new Error('forbidden flow name'));
|
||||
}
|
||||
return storageModule.getLibraryEntry(type, path);
|
||||
},
|
||||
saveLibraryEntry : function(type, path, meta, body) {
|
||||
saveLibraryEntry: function(type, path, meta, body) {
|
||||
if (is_malicious(path)) {
|
||||
return when.reject(new Error('forbidden flow name'));
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ var oldCredentialsFile;
|
||||
var userDir;
|
||||
var libDir;
|
||||
var libFlowsDir;
|
||||
var globalSettingsFile;
|
||||
|
||||
function listFiles(dir) {
|
||||
var dirs = {};
|
||||
@@ -140,6 +141,9 @@ var localfilesystem = {
|
||||
libDir = fspath.join(userDir,"lib");
|
||||
libFlowsDir = fspath.join(libDir,"flows");
|
||||
|
||||
|
||||
globalSettingsFile = fspath.join(userDir,".config.json");
|
||||
|
||||
return promiseDir(libFlowsDir);
|
||||
},
|
||||
|
||||
@@ -207,7 +211,20 @@ var localfilesystem = {
|
||||
|
||||
return nodeFn.call(fs.writeFile, credentialsFile, credentialData)
|
||||
},
|
||||
|
||||
|
||||
getSettings: function() {
|
||||
if (fs.existsSync(globalSettingsFile)) {
|
||||
return nodeFn.call(fs.readFile,globalSettingsFile,'utf8').then(function(data) {
|
||||
return JSON.parse(data);
|
||||
});
|
||||
}
|
||||
return when.resolve({});
|
||||
},
|
||||
saveSettings: function(settings) {
|
||||
return nodeFn.call(fs.writeFile,globalSettingsFile,JSON.stringify(settings),'utf8');
|
||||
},
|
||||
|
||||
|
||||
getAllFlows: function() {
|
||||
return listFiles(libFlowsDir);
|
||||
},
|
||||
|
Reference in New Issue
Block a user