diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7f67ba22..af804b1d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+#### 0.16.2: Maintenance Release
+
+ - Ensure custom mustache context parent set in Template node fixes #1126
+ - Display debug node name in debug panel if its known
+ - Ensure auth-tokens are removed when no user is specified in settings
+ - Ensure all a tags have blank target in info sidebar
+ - Ensure links do not span tabs in the editor
+ - Avoid creating multiple reconnect timers in websocket node
+ - Fix inner reference in install fail message catalog entry Fixes #1120
+ - Display buffer data properly for truncated buffers under Object property
+
#### 0.16.1: Maintenance Release
- Add colour swatches to debug when hex colour matched
diff --git a/editor/js/nodes.js b/editor/js/nodes.js
index 878b5fb02..8b400b417 100644
--- a/editor/js/nodes.js
+++ b/editor/js/nodes.js
@@ -1037,9 +1037,13 @@ RED.nodes = (function() {
var wires = (n.wires[w1] instanceof Array)?n.wires[w1]:[n.wires[w1]];
for (var w2=0;w2 Calls out to a system command. Provides 3 outputs: stdout, stderr, and return code. By default uses the
").appendTo(content);
if (!subflowNode && node.type != "comment") {
var helpText = $("script[data-help-name$='"+node.type+"']").html()||"";
- $('exec
system call which calls the command, then gets a callback
- on completion, returning the complete result in one message, along with any errors.
Optionally can use spawn
instead, which returns the output from stdout and stderr
as the command runs (usually one line at a time). On completion it then returns a return code
(on the 3rd output).
The exec
method spawns a subshell and therefore can be used for more complicated
+ commands involving pipes. However, it waits for completion of the whole command before returing anything.
The optional append gets added to the command after msg.payload
- so you can do
things like pipe the result to another command.
Commands or parameters with spaces should be enclosed in quotes - "This is a single parameter"
diff --git a/nodes/core/core/75-exec.js b/nodes/core/core/75-exec.js index 17551864d..f59175e81 100644 --- a/nodes/core/core/75-exec.js +++ b/nodes/core/core/75-exec.js @@ -115,12 +115,17 @@ module.exports = function(RED) { child = exec(cl, {encoding: 'binary', maxBuffer:10000000}, function (error, stdout, stderr) { msg.payload = new Buffer(stdout,"binary"); if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); } - var msg2 = {payload:stderr}; - var msg3 = null; + var msg2 = null; + if(stderr) { + msg2 = {payload: stderr}; + } + var msg3 = {payload:0}; + node.status({}); //console.log('[exec] stdout: ' + stdout); //console.log('[exec] stderr: ' + stderr); if (error !== null) { - msg3 = {payload:error}; + msg3 = {payload:error.code}; + node.status({fill:"red",shape:"dot",text:"error: "+error.code}); //console.log('[exec] error: ' + error); } if (!msg3) { node.status({}); } diff --git a/nodes/core/core/80-template.js b/nodes/core/core/80-template.js index 541a6b8a5..85efaccbe 100644 --- a/nodes/core/core/80-template.js +++ b/nodes/core/core/80-template.js @@ -22,8 +22,8 @@ module.exports = function(RED) { * Custom Mustache Context capable to resolve message property and node * flow and global context */ - function NodeContext(msg, nodeContext) { - this.msgContext = new mustache.Context(msg); + function NodeContext(msg, nodeContext,parent) { + this.msgContext = new mustache.Context(msg,parent); this.nodeContext = nodeContext; } @@ -31,26 +31,34 @@ module.exports = function(RED) { NodeContext.prototype.lookup = function (name) { // try message first: - var value = this.msgContext.lookup(name); - if (value !== undefined) { - return value; - } - - // try node context: - var dot = name.indexOf("."); - if (dot > 0) { - var contextName = name.substr(0, dot); - var variableName = name.substr(dot + 1); - - if (contextName === "flow" && this.nodeContext.flow) { - return this.nodeContext.flow.get(variableName); + try { + var value = this.msgContext.lookup(name); + if (value !== undefined) { + return value; } - else if (contextName === "global" && this.nodeContext.global) { - return this.nodeContext.global.get(variableName); + + // try node context: + var dot = name.indexOf("."); + if (dot > 0) { + var contextName = name.substr(0, dot); + var variableName = name.substr(dot + 1); + + if (contextName === "flow" && this.nodeContext.flow) { + return this.nodeContext.flow.get(variableName); + } + else if (contextName === "global" && this.nodeContext.global) { + return this.nodeContext.global.get(variableName); + } } + }catch(err) { + throw err; } } + NodeContext.prototype.push = function push (view) { + return new NodeContext(view, this.nodeContext,this.msgContext); + }; + function TemplateNode(n) { RED.nodes.createNode(this,n); this.name = n.name; @@ -65,7 +73,7 @@ module.exports = function(RED) { try { var value; if (node.syntax === "mustache") { - value = mustache.render(node.template, new NodeContext(msg, node.context())); + value = mustache.render(node.template,new NodeContext(msg, node.context())); } else { value = node.template; } diff --git a/nodes/core/core/lib/debug/debug-utils.js b/nodes/core/core/lib/debug/debug-utils.js index ad18dab2c..9c9f48661 100644 --- a/nodes/core/core/lib/debug/debug-utils.js +++ b/nodes/core/core/lib/debug/debug-utils.js @@ -205,7 +205,7 @@ RED.debug = (function() { var metaRow = $('').appendTo(msg); $(' ').appendTo(metaRow); if (sourceNode) { - $('',{href:"#",class:"debug-message-name"}).html('node: '+sourceNode.id) + $('',{href:"#",class:"debug-message-name"}).html('node: '+(sourceNode.name||sourceNode.id)) .appendTo(metaRow) .click(function(evt) { evt.preventDefault(); diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index a8d8fa27d..48ad858d3 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -84,7 +84,7 @@ module.exports = function(RED) { node.child.stdout.on('data', function (data) { var d = data.toString().trim().split("\n"); for (var i = 0; i < d.length; i++) { - if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i]))) { + if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { node.send({ topic:"pi/"+node.pin, payload:Number(d[i]) }); } node.buttonState = d[i]; diff --git a/nodes/core/hardware/nrgpio.py b/nodes/core/hardware/nrgpio.py index 207729f18..73f395fb0 100755 --- a/nodes/core/hardware/nrgpio.py +++ b/nodes/core/hardware/nrgpio.py @@ -99,9 +99,9 @@ if len(sys.argv) > 2: elif cmd == "in": #print "Initialised pin "+str(pin)+" to IN" - bounce = int(sys.argv[4]) + bounce = float(sys.argv[4]) def handle_callback(chan): - sleep(bounce/1000) + sleep(bounce/1000.0) print GPIO.input(chan) if sys.argv[3].lower() == "up": @@ -112,7 +112,7 @@ if len(sys.argv) > 2: GPIO.setup(pin,GPIO.IN) print GPIO.input(pin) - GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=bounce) + GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce)) while True: try: diff --git a/nodes/core/io/22-websocket.js b/nodes/core/io/22-websocket.js index 2723c1d60..fe6c9ed96 100644 --- a/nodes/core/io/22-websocket.js +++ b/nodes/core/io/22-websocket.js @@ -36,6 +36,7 @@ module.exports = function(RED) { node.closing = false; function startconn() { // Connect to remote endpoint + node.tout = null; var socket = new ws(node.path); socket.setMaxListeners(0); node.server = socket; // keep for closing @@ -52,6 +53,7 @@ module.exports = function(RED) { if (node.isServer) { delete node._clients[id]; node.emit('closed',Object.keys(node._clients).length); } else { node.emit('closed'); } if (!node.closing && !node.isServer) { + clearTimeout(node.tout); node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ? } }); @@ -61,6 +63,7 @@ module.exports = function(RED) { socket.on('error', function(err) { node.emit('erro'); if (!node.closing && !node.isServer) { + clearTimeout(node.tout); node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ? } }); @@ -123,7 +126,10 @@ module.exports = function(RED) { else { node.closing = true; node.server.close(); - if (node.tout) { clearTimeout(node.tout); } + if (node.tout) { + clearTimeout(node.tout); + node.tout = null; + } } }); } @@ -205,7 +211,9 @@ module.exports = function(RED) { this.error(RED._("websocket.errors.missing-conf")); } this.on('close', function() { - node.serverConfig.removeInputNode(node); + if (node.serverConfig) { + node.serverConfig.removeInputNode(node); + } node.status({}); }); } @@ -217,7 +225,7 @@ module.exports = function(RED) { this.server = (n.client)?n.client:n.server; this.serverConfig = RED.nodes.getNode(this.server); if (!this.serverConfig) { - this.error(RED._("websocket.errors.missing-conf")); + return this.error(RED._("websocket.errors.missing-conf")); } else { // TODO: nls diff --git a/nodes/core/io/31-tcpin.js b/nodes/core/io/31-tcpin.js index 709552f28..4a9650802 100644 --- a/nodes/core/io/31-tcpin.js +++ b/nodes/core/io/31-tcpin.js @@ -444,7 +444,7 @@ module.exports = function(RED) { //node.log(RED._("tcpin.errors.client-connected")); node.status({fill:"green",shape:"dot",text:"common.status.connected"}); if (clients[connection_id] && clients[connection_id].client) { - clients[connection_id].connected = true; + clients[connection_id].connected = true; clients[connection_id].client.write(clients[connection_id].msg.payload); } }); @@ -454,10 +454,10 @@ module.exports = function(RED) { } clients[connection_id].client.on('data', function(data) { - if (node.out == "sit") { // if we are staying connected just send the buffer + if (node.out === "sit") { // if we are staying connected just send the buffer if (clients[connection_id]) { clients[connection_id].msg.payload = data; - node.send(clients[connection_id].msg); + node.send(RED.util.cloneMessage(clients[connection_id].msg)); } } else if (node.splitc === 0) { @@ -533,7 +533,7 @@ module.exports = function(RED) { //console.log("END"); node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); if (clients[connection_id] && clients[connection_id].client) { - clients[connection_id].connected = false; + clients[connection_id].connected = false; clients[connection_id].client = null; } }); @@ -541,7 +541,7 @@ module.exports = function(RED) { clients[connection_id].client.on('close', function() { //console.log("CLOSE"); if (clients[connection_id]) { - clients[connection_id].connected = false; + clients[connection_id].connected = false; } var anyConnected = false; diff --git a/package.json b/package.json index f54f315a9..2cc364e8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name" : "node-red", - "version" : "0.16.1", + "version" : "0.16.2", "description" : "A visual tool for wiring the Internet of Things", "homepage" : "http://nodered.org", "license" : "Apache-2.0", diff --git a/red.js b/red.js index 8969f2e4c..2cea58be2 100755 --- a/red.js +++ b/red.js @@ -196,6 +196,7 @@ try { function basicAuthMiddleware(user,pass) { var basicAuth = require('basic-auth'); var checkPassword; + var localCachedPassword; if (pass.length == "32") { // Assume its a legacy md5 password checkPassword = function(p) { @@ -207,12 +208,26 @@ function basicAuthMiddleware(user,pass) { } } + var checkPasswordAndCache = function(p) { + // For BasicAuth routes we know the password cannot change without + // a restart of Node-RED. This means we can cache the provided crypted + // version to save recalculating each time. + if (localCachedPassword === p) { + return true; + } + var result = checkPassword(p); + if (result) { + localCachedPassword = p; + } + return result; + } + return function(req,res,next) { if (req.method === 'OPTIONS') { return next(); } var requestUser = basicAuth(req); - if (!requestUser || requestUser.name !== user || !checkPassword(requestUser.pass)) { + if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) { res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); return res.sendStatus(401); } diff --git a/red/runtime/locales/en-US/runtime.json b/red/runtime/locales/en-US/runtime.json index 553cf5536..fa5d6088c 100644 --- a/red/runtime/locales/en-US/runtime.json +++ b/red/runtime/locales/en-US/runtime.json @@ -32,6 +32,7 @@ "upgrading": "Upgrading module: __name__ to version: __version__", "upgraded": "Upgraded module: __name__. Restart Node-RED to use the new version", "upgrade-failed-not-found": "$t(server.install.install-failed-long) version not found", + "install-failed-not-found": "$t(server.install.install-failed-long) module not found", "uninstalling": "Uninstalling module: __name__", "uninstall-failed": "Uninstall failed", "uninstall-failed-long": "Uninstall of module __name__ failed:", diff --git a/red/runtime/log.js b/red/runtime/log.js index d2e28726e..259ae4000 100644 --- a/red/runtime/log.js +++ b/red/runtime/log.js @@ -44,6 +44,8 @@ var levelNames = { var logHandlers = []; +var verbose; + var metricsEnabled = false; var LogHandler = function(settings) { @@ -72,11 +74,15 @@ var consoleLogger = function(msg) { if (msg.level == log.METRIC || msg.level == log.AUDIT) { util.log("["+levelNames[msg.level]+"] "+JSON.stringify(msg)); } else { - var message = msg.msg; - if (typeof message === 'object' && message.toString() === '[object Object]' && message.message) { - message = message.message; + if (verbose && msg.msg.stack) { + util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+msg.msg.stack); + } else { + var message = msg.msg; + if (typeof message === 'object' && message.toString() === '[object Object]' && message.message) { + message = message.message; + } + util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message); } - util.log("["+levelNames[msg.level]+"] "+(msg.type?"["+msg.type+":"+(msg.name||msg.id)+"] ":"")+message); } } @@ -94,6 +100,7 @@ var log = module.exports = { metricsEnabled = false; logHandlers = []; var loggerSettings = {}; + verbose = settings.verbose; if (settings.logging) { var keys = Object.keys(settings.logging); if (keys.length === 0) { diff --git a/red/runtime/nodes/registry/loader.js b/red/runtime/nodes/registry/loader.js index dd4292c23..124fd8e2e 100644 --- a/red/runtime/nodes/registry/loader.js +++ b/red/runtime/nodes/registry/loader.js @@ -315,6 +315,18 @@ function loadNodeSet(node) { return loadPromise; } catch(err) { node.err = err; + var stack = err.stack; + var message; + if (stack) { + var i = stack.indexOf(node.file); + if (i > -1) { + var excerpt = stack.substring(i+node.file.length+1,i+node.file.length+20); + var m = /^(\d+):(\d+)/.exec(excerpt); + if (m) { + node.err = err+" (line:"+m[1]+")"; + } + } + } return when.resolve(node); } } @@ -381,8 +393,10 @@ function getNodeHelp(node,lang) { } if (help) { node.help[lang] = help; + } else if (lang === runtime.i18n.defaultLang) { + return null; } else { - node.help[lang] = node.help[runtime.i18n.defaultLang]; + node.help[lang] = getNodeHelp(node, runtime.i18n.defaultLang); } } return node.help[lang]; diff --git a/test/nodes/core/core/80-template_spec.js b/test/nodes/core/core/80-template_spec.js index 315aef6ae..811e7f179 100644 --- a/test/nodes/core/core/80-template_spec.js +++ b/test/nodes/core/core/80-template_spec.js @@ -166,6 +166,18 @@ describe('template node', function() { }); }); + it('should handle block contexts objects', function(done) { + var flow = [{id:"n1", type:"template", template: "A{{#payload.A}}{{payload.A}}{{.}}{{/payload.A}}B",wires:[["n2"]]},{id:"n2",type:"helper"}]; + helper.load(templateNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('payload','AabcabcB'); + done(); + }); + n1.receive({payload:{A:"abc"}}); + }); + }); it('should raise error if passed bad template', function(done) { var flow = [{id:"n1", type:"template", field: "payload", template: "payload={{payload",wires:[["n2"]]},{id:"n2",type:"helper"}]; helper.load(templateNode, flow, function() { diff --git a/test/red/runtime/nodes/registry/loader_spec.js b/test/red/runtime/nodes/registry/loader_spec.js index 39b25d55e..87e1c060b 100644 --- a/test/red/runtime/nodes/registry/loader_spec.js +++ b/test/red/runtime/nodes/registry/loader_spec.js @@ -514,7 +514,7 @@ describe("red/nodes/registry/loader",function() { node.enabled.should.be.true(); nodes.registerType.called.should.be.false(); node.should.have.property('err'); - node.err.message.should.eql("fail to require"); + node.err.toString().should.eql("Error: fail to require (line:1)"); done(); }).otherwise(function(err) { @@ -564,6 +564,45 @@ describe("red/nodes/registry/loader",function() { loader.getNodeHelp(node,"fr").should.eql("foo"); fs.readFileSync.calledOnce.should.be.true(); }); + it("loads help, defaulting to en-US content for extra nodes", function() { + stubs.push(sinon.stub(fs,"readFileSync", function(path) { + if (path.indexOf("en-US") >= 0) { + return 'bar'; + } + throw new Error("not found"); + })); + var node = { + template: "/tmp/node/directory/file.html", + help:{} + }; + delete node.help['en-US']; + + loader.getNodeHelp(node,"fr").should.eql("bar"); + node.help['fr'].should.eql("bar"); + fs.readFileSync.calledTwice.should.be.true(); + fs.readFileSync.firstCall.args[0].should.eql(path.normalize("/tmp/node/directory/locales/fr/file.html")); + fs.readFileSync.lastCall.args[0].should.eql(path.normalize("/tmp/node/directory/locales/en-US/file.html")); + loader.getNodeHelp(node,"fr").should.eql("bar"); + fs.readFileSync.calledTwice.should.be.true(); + }); + it("fails to load en-US help content", function() { + stubs.push(sinon.stub(fs,"readFileSync", function(path) { + throw new Error("not found"); + })); + var node = { + template: "/tmp/node/directory/file.html", + help:{} + }; + delete node.help['en-US']; + + should.not.exist(loader.getNodeHelp(node,"en-US")); + should.not.exist(node.help['en-US']); + fs.readFileSync.calledTwice.should.be.true(); + fs.readFileSync.firstCall.args[0].should.eql(path.normalize("/tmp/node/directory/locales/en-US/file.html")); + fs.readFileSync.lastCall.args[0].should.eql(path.normalize("/tmp/node/directory/locales/en/file.html")); + should.not.exist(loader.getNodeHelp(node,"en-US")); + fs.readFileSync.callCount.should.eql(4); + }); }); });