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:
Nick O'Leary
2014-08-28 00:35:07 +01:00
parent 00cb8d5bce
commit da61fe12d0
24 changed files with 1540 additions and 381 deletions

179
red/bin/nr-cli.js Executable file
View 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);
}
});
}
})();

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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
View 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;

View File

@@ -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'));
}

View File

@@ -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);
},