diff --git a/red/comms.js b/red/comms.js index 8f56a1990..eb20f678c 100644 --- a/red/comms.js +++ b/red/comms.js @@ -40,12 +40,11 @@ function start() { var Tokens = require("./api/auth/tokens"); var Users = require("./api/auth/users"); var Permissions = require("./api/auth/permissions"); - if (!settings.disableEditor) { Users.default().then(function(anonymousUser) { var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000; var path = settings.httpAdminRoot || "/"; - path = path + (path.slice(-1) == "/" ? "":"/") + "comms"; + path = (path.slice(0,1) != "/" ? "/":"") + path + (path.slice(-1) == "/" ? "":"/") + "comms"; wsServer = new ws.Server({server:server,path:path}); wsServer.on('connection',function(ws) { @@ -127,9 +126,11 @@ function start() { function stop() { if (heartbeatTimer) { clearInterval(heartbeatTimer); + heartbeatTimer = null; } if (wsServer) { wsServer.close(); + wsServer = null; } } diff --git a/red/log.js b/red/log.js index ccaa8e226..f41ce4fb3 100644 --- a/red/log.js +++ b/red/log.js @@ -47,7 +47,6 @@ var ConsoleLogHandler = function(settings) { this.metricsOn = settings.metrics||false; metricsEnabled = this.metricsOn; this.on("log",function(msg) { - /* istanbul ignore else */ if (this.shouldReportMessage(msg.level)) { if (msg.level == log.METRIC) { util.log("[metric] "+JSON.stringify(msg)); diff --git a/red/server.js b/red/server.js index 46c312706..1ee63d278 100644 --- a/red/server.js +++ b/red/server.js @@ -54,7 +54,7 @@ function start() { if (log.metric()) { runtimeMetricInterval = setInterval(function() { reportMetrics(); - }, 15000); + }, settings.runtimeMetricInterval||15000); } console.log("\n\nWelcome to Node-RED\n===================\n"); if (settings.version) { @@ -92,7 +92,7 @@ function start() { if (missingModules.hasOwnProperty(i)) { log.warn(" - "+i+": "+missingModules[i].join(", ")); if (settings.autoInstallModules && i != "node-red") { - installModule(i).otherwise(function(err) { + serverAPI.installModule(i).otherwise(function(err) { // Error already reported. Need the otherwise handler // to stop the error propagating any further }); @@ -216,22 +216,21 @@ function uninstallModule(module) { function reportMetrics() { var memUsage = process.memoryUsage(); - // only need to init these once per report - var metrics = {}; - metrics.level = log.METRIC; - - //report it - metrics.event = "runtime.memory.rss" - metrics.value = memUsage.rss; - log.log(metrics); - - metrics.event = "runtime.memory.heapTotal" - metrics.value = memUsage.heapTotal; - log.log(metrics); - - metrics.event = "runtime.memory.heapUsed" - metrics.value = memUsage.heapUsed; - log.log(metrics); + 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 + }); } function stop() { @@ -243,7 +242,7 @@ function stop() { comms.stop(); } -module.exports = { +var serverAPI = module.exports = { init: init, start: start, stop: stop, diff --git a/test/red/comms_spec.js b/test/red/comms_spec.js index b4f9a1d1e..940d6587a 100644 --- a/test/red/comms_spec.js +++ b/test/red/comms_spec.js @@ -135,7 +135,138 @@ describe("comms", function() { }); } }); - + describe("disabled editor", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {disableEditor:true}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/comms'; + comms.start(); + done(); + }); + }); + + after(function() { + comms.stop(); + }); + + it('rejects websocket connections',function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + done(new Error("Socket connection unexpectedly accepted")); + ws.close(); + }); + ws.on('error', function() { + done(); + }); + + }); + }); + + describe("non-default httpAdminRoot set: /adminPath", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {httpAdminRoot:"/adminPath"}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/adminPath/comms'; + comms.start(); + done(); + }); + }); + + after(function() { + comms.stop(); + }); + + it('accepts connections',function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.close(); + done(); + }); + ws.on('error', function() { + done(new Error("Socket connection failed")); + }); + + }); + }); + + describe("non-default httpAdminRoot set: /adminPath/", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {httpAdminRoot:"/adminPath/"}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/adminPath/comms'; + comms.start(); + done(); + }); + }); + + after(function() { + comms.stop(); + }); + + it('accepts connections',function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.close(); + done(); + }); + ws.on('error', function() { + done(new Error("Socket connection failed")); + }); + + }); + }); + + describe("non-default httpAdminRoot set: adminPath", function() { + var server; + var url; + var port; + before(function(done) { + server = http.createServer(function(req,res){app(req,res)}); + comms.init(server, {httpAdminRoot:"adminPath"}); + server.listen(listenPort, address); + server.on('listening', function() { + port = server.address().port; + url = 'http://' + address + ':' + port + '/adminPath/comms'; + comms.start(); + done(); + }); + }); + + after(function() { + comms.stop(); + }); + + it('accepts connections',function(done) { + var ws = new WebSocket(url); + ws.on('open', function() { + ws.close(); + done(); + }); + ws.on('error', function() { + done(new Error("Socket connection failed")); + }); + + }); + }); + describe("keep alives", function() { var server; var url; diff --git a/test/red/log_spec.js b/test/red/log_spec.js index e2d7bf87f..1eadc6848 100644 --- a/test/red/log_spec.js +++ b/test/red/log_spec.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 IBM Corp. + * Copyright 2014, 205 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,17 +17,17 @@ var should = require("should"); var sinon = require("sinon"); var util = require("util"); +var log = require("../../red/log"); + describe("red/log", function() { it('can be required without errors', function() { require("../../red/log"); }); - var log = require("../../red/log"); - var sett = {logging: { console: { level: 'metric', metrics: true } } }; - log.init(sett); - beforeEach(function () { - var spy = sinon.spy(util, 'log'); + var spy = sinon.stub(util, 'log', function(arg){}); + var settings = {logging: { console: { level: 'metric', metrics: true } } }; + log.init(settings); }); afterEach(function() { @@ -36,17 +36,27 @@ describe("red/log", function() { it('it can raise an error', function() { var ret = log.error("This is an error"); - sinon.assert.calledWithMatch(util.log,""); + sinon.assert.calledWithMatch(util.log,"[error] This is an error"); }); it('it can raise a trace', function() { var ret = log.trace("This is a trace"); - sinon.assert.calledWithMatch(util.log,""); + sinon.assert.calledWithMatch(util.log,"[trace] This is a trace"); }); it('it can raise a debug', function() { var ret = log.debug("This is a debug"); - sinon.assert.calledWithMatch(util.log,""); + sinon.assert.calledWithMatch(util.log,"[debug] This is a debug"); + }); + + it('it can raise a info', function() { + var ret = log.info("This is an info"); + sinon.assert.calledWithMatch(util.log,"[info] This is an info"); + }); + + it('it can raise a warn', function() { + var ret = log.warn("This is a warn"); + sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); }); it('it can raise a metric', function() { @@ -57,17 +67,81 @@ describe("red/log", function() { metrics.msgid = "12345"; metrics.value = "the metric payload"; var ret = log.log(metrics); - sinon.assert.calledWithMatch(util.log,""); + util.log.calledOnce.should.be.true; + util.log.firstCall.args[0].indexOf("[metric] ").should.equal(0); + var body = JSON.parse(util.log.firstCall.args[0].substring(9)); + body.should.have.a.property("nodeid","testid"); + body.should.have.a.property("event","node.test.testevent"); + body.should.have.a.property("msgid","12345"); + body.should.have.a.property("value","the metric payload"); + body.should.have.a.property("timestamp"); + body.should.have.a.property("level",log.METRIC); }); it('it checks metrics are enabled', function() { log.metric().should.equal(true); var sett = {logging: { console: { level: 'info', metrics: false } } }; log.init(sett); - }); - - it('it checks metrics are disabled', function() { log.metric().should.equal(false); }); + it('it logs node type and name if provided',function() { + log.log({level:log.INFO,type:"nodeType",msg:"test",name:"nodeName",id:"nodeId"}); + util.log.calledOnce.should.be.true; + util.log.firstCall.args[0].indexOf("[nodeType:nodeName]").should.not.equal(-1); + }); + it('it logs node type and id if no name provided',function() { + log.log({level:log.INFO,type:"nodeType",msg:"test",id:"nodeId"}); + util.log.calledOnce.should.be.true; + util.log.firstCall.args[0].indexOf("[nodeType:nodeId]").should.not.equal(-1); + }); + + it('ignores lower level messages and metrics', function() { + var settings = {logging: { console: { level: 'warn', metrics: false } } }; + log.init(settings); + log.error("This is an error"); + log.warn("This is a warn"); + log.info("This is an info"); + log.debug("This is a debug"); + log.trace("This is a trace"); + log.log({level:log.METRIC,msg:"testMetric"}); + sinon.assert.calledWithMatch(util.log,"[error] This is an error"); + sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); + sinon.assert.neverCalledWithMatch(util.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); + sinon.assert.neverCalledWithMatch(util.log,"[metric] "); + }); + it('ignores lower level messages but accepts metrics', function() { + var settings = {logging: { console: { level: 'log', metrics: true } } }; + log.init(settings); + log.error("This is an error"); + log.warn("This is a warn"); + log.info("This is an info"); + log.debug("This is a debug"); + log.trace("This is a trace"); + log.log({level:log.METRIC,msg:"testMetric"}); + sinon.assert.calledWithMatch(util.log,"[error] This is an error"); + sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); + sinon.assert.calledWithMatch(util.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); + sinon.assert.calledWithMatch(util.log,"[metric] "); + }); + + it('default settings set to INFO and metrics off', function() { + log.init({logging:{}}); + log.error("This is an error"); + log.warn("This is a warn"); + log.info("This is an info"); + log.debug("This is a debug"); + log.trace("This is a trace"); + log.log({level:log.METRIC,msg:"testMetric"}); + sinon.assert.calledWithMatch(util.log,"[error] This is an error"); + sinon.assert.calledWithMatch(util.log,"[warn] This is a warn"); + sinon.assert.calledWithMatch(util.log,"[info] This is an info"); + sinon.assert.neverCalledWithMatch(util.log,"[debug] This is a debug"); + sinon.assert.neverCalledWithMatch(util.log,"[trace] This is a trace"); + sinon.assert.neverCalledWithMatch(util.log,"[metric] "); + }); }); diff --git a/test/red/server_spec.js b/test/red/server_spec.js index 4e58f55a7..bc567e683 100644 --- a/test/red/server_spec.js +++ b/test/red/server_spec.js @@ -1,5 +1,5 @@ /** - * Copyright 2014 IBM Corp. + * Copyright 2014, 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,9 @@ var comms = require("../../red/comms"); var redNodes = require("../../red/nodes"); var api = require("../../red/api"); var server = require("../../red/server"); - +var storage = require("../../red/storage"); +var settings = require("../../red/settings"); +var log = require("../../red/log"); describe("red/server", function() { var commsMessages = []; @@ -44,9 +46,8 @@ describe("red/server", function() { it("initialises components", function() { var commsInit = sinon.stub(comms,"init",function() {}); - var dummyServer = {}; - server.init(dummyServer,{httpAdminRoot:"/"}); + server.init(dummyServer,{testSettings: true, httpAdminRoot:"/", load:function() { return when.resolve();}}); commsInit.called.should.be.true; @@ -58,6 +59,173 @@ describe("red/server", function() { commsInit.restore(); }); + describe("start",function() { + var commsInit; + var storageInit; + var settingsLoad; + var apiInit; + var logMetric; + var logWarn; + var logInfo; + var logLog; + var redNodesInit; + var redNodesLoad; + var redNodesCleanModuleList; + var redNodesGetNodeList; + var redNodesLoadFlows; + var commsStart; + + beforeEach(function() { + commsInit = sinon.stub(comms,"init",function() {}); + storageInit = sinon.stub(storage,"init",function(settings) {return when.resolve();}); + apiInit = sinon.stub(api,"init",function() {}); + logMetric = sinon.stub(log,"metric",function() { return false; }); + logWarn = sinon.stub(log,"warn",function() { }); + logInfo = sinon.stub(log,"info",function() { }); + logLog = sinon.stub(log,"log",function(m) {}); + redNodesInit = sinon.stub(redNodes,"init", function() {}); + redNodesLoad = sinon.stub(redNodes,"load", function() {return when.resolve()}); + redNodesCleanModuleList = sinon.stub(redNodes,"cleanModuleList",function(){}); + redNodesLoadFlows = sinon.stub(redNodes,"loadFlows",function() {}); + commsStart = sinon.stub(comms,"start",function(){}); + }); + afterEach(function() { + commsInit.restore(); + storageInit.restore(); + apiInit.restore(); + logMetric.restore(); + logWarn.restore(); + logInfo.restore(); + logLog.restore(); + redNodesInit.restore(); + redNodesLoad.restore(); + redNodesGetNodeList.restore(); + redNodesCleanModuleList.restore(); + redNodesLoadFlows.restore(); + commsStart.restore(); + }); + it("reports errored/missing modules",function(done) { + redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() { + return [ + { err:"errored",name:"errName" }, // error + { module:"module",enabled:true,loaded:false,types:["typeA","typeB"]} // missing + ]; + }); + server.init({},{testSettings: true, httpAdminRoot:"/", load:function() { return when.resolve();}}); + server.start().then(function() { + try { + apiInit.calledOnce.should.be.true; + storageInit.calledOnce.should.be.true; + redNodesInit.calledOnce.should.be.true; + redNodesLoad.calledOnce.should.be.true; + commsStart.calledOnce.should.be.true; + redNodesLoadFlows.calledOnce.should.be.true; + + logWarn.calledWithMatch("Failed to register 1 node type"); + logWarn.calledWithMatch("Missing node modules"); + logWarn.calledWithMatch(" - module: typeA, typeB"); + redNodesCleanModuleList.calledOnce.should.be.true; + done(); + } catch(err) { + done(err); + } + }); + }); + it("initiates load of missing modules",function(done) { + redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() { + return [ + { err:"errored",name:"errName" }, // error + { err:"errored",name:"errName" }, // error + { module:"module",enabled:true,loaded:false,types:["typeA","typeB"]}, // missing + { module:"node-red",enabled:true,loaded:false,types:["typeC","typeD"]} // missing + ]; + }); + var serverInstallModule = sinon.stub(server,"installModule",function(name) { return when.resolve();}); + server.init({},{testSettings: true, autoInstallModules:true, httpAdminRoot:"/", load:function() { return when.resolve();}}); + server.start().then(function() { + try { + apiInit.calledOnce.should.be.true; + logWarn.calledWithMatch("Failed to register 2 node types"); + logWarn.calledWithMatch("Missing node modules"); + logWarn.calledWithMatch(" - module: typeA, typeB"); + logWarn.calledWithMatch(" - node-red: typeC, typeD"); + redNodesCleanModuleList.calledOnce.should.be.false; + serverInstallModule.calledOnce.should.be.true; + serverInstallModule.calledWithMatch("module"); + done(); + } catch(err) { + done(err); + } finally { + serverInstallModule.restore(); + } + }); + }); + it("reports errored modules when verbose is enabled",function(done) { + redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() { + return [ + { err:"errored",name:"errName" } // error + ]; + }); + server.init({},{testSettings: true, verbose:true, httpAdminRoot:"/", load:function() { return when.resolve();}}); + server.start().then(function() { + + try { + apiInit.calledOnce.should.be.true; + logWarn.neverCalledWithMatch("Failed to register 1 node type"); + logWarn.calledWithMatch("[errName] errored"); + done(); + } catch(err) { + done(err); + } + }); + }); + + it("reports runtime metrics",function(done) { + var commsStop = sinon.stub(comms,"stop",function() {} ); + var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} ); + redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() {return []}); + logMetric.restore(); + logMetric = sinon.stub(log,"metric",function() { return true; }); + server.init({},{testSettings: true, runtimeMetricInterval:400, httpAdminRoot:"/", load:function() { return when.resolve();}}); + server.start().then(function() { + setTimeout(function() { + try { + apiInit.calledOnce.should.be.true; + logLog.args.should.have.lengthOf(3); + logLog.args[0][0].should.have.property("level",log.METRIC); + logLog.args[0][0].should.have.property("event","runtime.memory.rss"); + logLog.args[1][0].should.have.property("level",log.METRIC); + logLog.args[1][0].should.have.property("event","runtime.memory.heapTotal"); + logLog.args[2][0].should.have.property("level",log.METRIC); + logLog.args[2][0].should.have.property("event","runtime.memory.heapUsed"); + done(); + } catch(err) { + done(err); + } finally { + server.stop(); + commsStop.restore(); + stopFlows.restore(); + } + },500); + }); + }); + + it("doesn't init api if httpAdminRoot set to false",function(done) { + redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() {return []}); + server.init({},{testSettings: true, httpAdminRoot:false, load:function() { return when.resolve();}}); + server.start().then(function() { + setTimeout(function() { + try { + apiInit.calledOnce.should.be.false; + done(); + } catch(err) { + done(err); + } + },500); + }); + }); + }); + it("stops components", function() { var commsStop = sinon.stub(comms,"stop",function() {} ); var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} ); diff --git a/test/red/settings_spec.js b/test/red/settings_spec.js index 3c4fb9600..7e7e3f4b2 100644 --- a/test/red/settings_spec.js +++ b/test/red/settings_spec.js @@ -80,11 +80,13 @@ describe("red/settings", function() { c: [1,2,3] } var savedSettings = null; + var saveCount = 0; var storage = { getSettings: function() { return when.resolve({globalA:789}); }, saveSettings: function(settings) { + saveCount++; savedSettings = settings; return when.resolve(); } @@ -102,8 +104,14 @@ describe("red/settings", function() { settings.available().should.be.true; settings.get("globalA").should.equal(789); settings.set("globalA","abc").then(function() { + savedSettings.globalA.should.equal("abc"); + saveCount.should.equal(1); + settings.set("globalA","abc").then(function() { savedSettings.globalA.should.equal("abc"); + // setting to existing value should not trigger save + saveCount.should.equal(1); done(); + }); }); }).otherwise(function(err) { done(err);