Skip to content

Commit 437c295

Browse files
authored
new features: Add Triggered Mode to Ping Node (#643)
1 parent 3087e8e commit 437c295

File tree

7 files changed

+336
-60
lines changed

7 files changed

+336
-60
lines changed

io/ping/88-ping.html

+77-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11

2-
<script type="text/x-red" data-template-name="ping">
3-
<div class="form-row">
2+
<script type="text/html" data-template-name="ping">
3+
<div class="form-row" id="div-node-input-host">
44
<label for="node-input-host"><i class="fa fa-dot-circle-o"></i> <span data-i18n="ping.label.target"></span></label>
5-
<input type="text" id="node-input-host" placeholder="www.google.com">
5+
<input type="text" id="node-input-host" placeholder="192.168.0.1, www.google.com">
66
</div>
77
<div class="form-row">
8+
<label for="node-input-mode"><i class="fa fa-wrench"></i> <span data-i18n="ping.label.mode"></label>
9+
<select type="text" id="node-input-mode" style="width: 70%">
10+
<option value="timed" data-i18n="ping.label.mode_option.timed"></option>
11+
<option value="triggered" data-i18n="ping.label.mode_option.triggered"></option>
12+
</select>
13+
</div>
14+
<div class="form-row" id="div-node-input-timer">
815
<label for="node-input-timer"><i class="fa fa-repeat"></i> <span data-i18n="ping.label.ping"></label>
916
<input type="text" id="node-input-timer" placeholder="20">
1017
</div>
@@ -15,13 +22,48 @@
1522
</script>
1623

1724
<script type="text/javascript">
18-
RED.nodes.registerType('ping',{
19-
category: 'network-input',
25+
26+
var timerParameterValidator = function(node,v){
27+
var mode = getMode(node)
28+
if(mode === "triggered"){
29+
return true;
30+
}
31+
if(v == ""){
32+
return false;
33+
}
34+
return RED.validators.number()(v);
35+
}
36+
var hostParameterValidator = function(node,v){
37+
var mode = getMode(node)
38+
if(mode === "triggered"){
39+
return true;
40+
}
41+
return v != ""
42+
}
43+
var getMode = function(node){
44+
if(node){
45+
return node.mode
46+
} else {
47+
let $mode = $( "#node-input-mode" );
48+
return $mode.val();
49+
}
50+
}
51+
52+
RED.nodes.registerType("ping",{
53+
category: "network-input",
2054
color:"#fdf0c2",
2155
defaults: {
56+
mode: {value:"timed"},
2257
name: {value:""},
23-
host: {value:"",required:true},
24-
timer: {value:"20", required:true, validate:RED.validators.number()}
58+
host: {value:"", validate: function(v){
59+
return hostParameterValidator(this,v) ;
60+
}
61+
},
62+
timer: {value:"20", validate: function(v){
63+
return timerParameterValidator(this,v) ;
64+
}
65+
},
66+
inputs: {value:0}
2567
},
2668
inputs:0,
2769
outputs:1,
@@ -30,10 +72,37 @@
3072
return this._("ping.ping");
3173
},
3274
label: function() {
33-
return this.name||this.host;
75+
let lbl = this.name||this.host||this._("ping.ping");
76+
if(lbl.length > 20){
77+
lbl = lbl.substring(0,17) + "..."
78+
}
79+
return lbl;
3480
},
3581
labelStyle: function() {
3682
return this.name?"node_label_italic":"";
83+
},
84+
oneditprepare: function () {
85+
let node = this;
86+
let $timer = $("#div-node-input-timer");
87+
$("#node-input-mode").val(node.mode);
88+
let $mode = $( "#node-input-mode" );
89+
function updateControlsVisibility(){
90+
node.mode = $mode.val();
91+
switch (node.mode) {
92+
case "triggered":
93+
node.inputs = 1;
94+
node._def.inputs = 1
95+
$timer.hide();
96+
break;
97+
default:
98+
node.inputs = 0;
99+
node._def.inputs = 0;
100+
$timer.show();
101+
break;
102+
}
103+
}
104+
$mode.change(updateControlsVisibility);
105+
updateControlsVisibility();
37106
}
38107
});
39108
</script>

io/ping/88-ping.js

+116-43
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,134 @@
11

22
module.exports = function(RED) {
33
"use strict";
4-
var spawn = require('child_process').spawn;
5-
var plat = require('os').platform();
4+
var spawn = require("child_process").spawn;
5+
var plat = require("os").platform();
6+
7+
function doPing(node, host, arrayMode){
8+
const defTimeout = 5000;
9+
var ex, hostOptions, commandLineOptions;
10+
if(typeof host === "string"){
11+
hostOptions = {
12+
host: host,
13+
timeout: defTimeout
14+
}
15+
} else {
16+
hostOptions = host;
17+
hostOptions.timeout = isNaN(parseInt(hostOptions.timeout)) ? defTimeout : parseInt(hostOptions.timeout);
18+
}
19+
//clamp timeout between 1 and 30 sec
20+
hostOptions.timeout = hostOptions.timeout < 1000 ? 1000 : hostOptions.timeout;
21+
hostOptions.timeout = hostOptions.timeout > 30000 ? 30000 : hostOptions.timeout;
22+
var timeoutS = Math.round(hostOptions.timeout / 1000); //whole numbers only
23+
var msg = { payload:false, topic:hostOptions.host };
24+
//only include the extra msg object if operating in advance/array mode.
25+
if(arrayMode){
26+
msg.ping = hostOptions
27+
}
28+
if (plat == "linux" || plat == "android") {
29+
commandLineOptions = ["-n", "-w", timeoutS, "-c", "1"]
30+
} else if (plat.match(/^win/)) {
31+
commandLineOptions = ["-n", "1", "-w", hostOptions.timeout]
32+
} else if (plat == "darwin" || plat == "freebsd") {
33+
commandLineOptions = ["-n", "-t", timeoutS, "-c", "1"]
34+
} else {
35+
node.error("Sorry - your platform - "+plat+" - is not recognised.", msg);
36+
return; //dont pass go - just return!
37+
}
38+
39+
//spawn with timeout in case of os issue
40+
ex = spawn("ping", [...commandLineOptions, hostOptions.host]);
41+
42+
//monitor every spawned process & SIGINT if too long
43+
var spawnTout = setTimeout(() => {
44+
node.log(`ping - Host '${hostOptions.host}' process timeout - sending SIGINT`)
45+
try{if(ex && ex.pid){ ex.kill("SIGINT"); }} catch(e){console.warn(e)}
46+
}, hostOptions.timeout+1000); //add 1s for grace
47+
48+
var res = false;
49+
var line = "";
50+
var fail = false;
51+
//var regex = /from.*time.(.*)ms/;
52+
var regex = /=.*[<|=]([0-9]*).*TTL|ttl..*=([0-9\.]*)/;
53+
ex.stdout.on("data", function (data) {
54+
line += data.toString();
55+
});
56+
ex.on("exit", function (err) {
57+
clearTimeout(spawnTout);
58+
});
59+
ex.on("error", function (err) {
60+
fail = true;
61+
if (err.code === "ENOENT") {
62+
node.error(err.code + " ping command not found", msg);
63+
}
64+
else if (err.code === "EACCES") {
65+
node.error(err.code + " can't run ping command", msg);
66+
}
67+
else {
68+
node.error(err.code, msg);
69+
}
70+
});
71+
ex.on("close", function (code) {
72+
if (fail) { fail = false; return; }
73+
var m = regex.exec(line)||"";
74+
if (m !== "") {
75+
if (m[1]) { res = Number(m[1]); }
76+
if (m[2]) { res = Number(m[2]); }
77+
}
78+
if (code === 0) { msg.payload = res }
79+
try { node.send(msg); }
80+
catch(e) {console.warn(e)}
81+
});
82+
}
683

784
function PingNode(n) {
885
RED.nodes.createNode(this,n);
86+
this.mode = n.mode;
987
this.host = n.host;
1088
this.timer = n.timer * 1000;
1189
var node = this;
1290

13-
node.tout = setInterval(function() {
14-
var ex;
15-
if (plat == "linux" || plat == "android") { ex = spawn('ping', ['-n', '-w', '5', '-c', '1', node.host]); }
16-
else if (plat.match(/^win/)) { ex = spawn('ping', ['-n', '1', '-w', '5000', node.host]); }
17-
else if (plat == "darwin" || plat == "freebsd") { ex = spawn('ping', ['-n', '-t', '5', '-c', '1', node.host]); }
18-
else { node.error("Sorry - your platform - "+plat+" - is not recognised."); }
19-
var res = false;
20-
var line = "";
21-
var fail = false;
22-
//var regex = /from.*time.(.*)ms/;
23-
var regex = /=.*[<|=]([0-9]*).*TTL|ttl..*=([0-9\.]*)/;
24-
ex.stdout.on('data', function (data) {
25-
line += data.toString();
26-
});
27-
//ex.stderr.on('data', function (data) {
28-
//console.log('[ping] stderr: ' + data);
29-
//});
30-
ex.on('error', function (err) {
31-
fail = true;
32-
if (err.code === "ENOENT") {
33-
node.error(err.code + " ping command not found");
34-
}
35-
else if (err.code === "EACCES") {
36-
node.error(err.code + " can't run ping command");
91+
function generatePingList(str) {
92+
return (str + "").split(",").map((e) => (e + "").trim()).filter((e) => e != "");
93+
}
94+
function clearPingInterval(){
95+
if (node.tout) { clearInterval(node.tout); }
96+
}
97+
98+
if(node.mode === "triggered"){
99+
clearPingInterval();
100+
} else if(node.timer){
101+
node.tout = setInterval(function() {
102+
let pingables = generatePingList(node.host);
103+
for (let index = 0; index < pingables.length; index++) {
104+
const element = pingables[index];
105+
if(element){ doPing(node, element, false); }
37106
}
38-
else {
39-
node.error(err.code);
107+
}, node.timer);
108+
}
109+
110+
this.on("input", function (msg) {
111+
let node = this;
112+
let payload = node.host || msg.payload;
113+
if(typeof payload == "string"){
114+
let pingables = generatePingList(payload)
115+
for (let index = 0; index < pingables.length; index++) {
116+
const element = pingables[index];
117+
if(element){ doPing(node, element, false); }
40118
}
41-
});
42-
ex.on('close', function (code) {
43-
if (fail) { fail = false; return; }
44-
var m = regex.exec(line)||"";
45-
if (m !== '') {
46-
if (m[1]) { res = Number(m[1]); }
47-
if (m[2]) { res = Number(m[2]); }
119+
} else if (Array.isArray(payload) ) {
120+
for (let index = 0; index < payload.length; index++) {
121+
const element = payload[index];
122+
if(element){ doPing(node, element, true); }
48123
}
49-
var msg = { payload:false, topic:node.host };
50-
if (code === 0) { msg = { payload:res, topic:node.host }; }
51-
try { node.send(msg); }
52-
catch(e) {}
53-
});
54-
}, node.timer);
124+
}
125+
});
55126

56127
this.on("close", function() {
57-
if (this.tout) { clearInterval(this.tout); }
128+
clearPingInterval();
58129
});
130+
131+
59132
}
60133
RED.nodes.registerType("ping",PingNode);
61-
}
134+
}

io/ping/README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,17 @@ The fix is to allow it as follows
2727
Usage
2828
-----
2929

30-
Pings a machine and returns the trip time in mS as `msg.payload`.
30+
Pings 1 or more devices and returns the trip time in mS as `msg.payload`.
3131

3232
Returns boolean `false` if no response received, or if the host is unresolveable.
3333

3434
`msg.error` will contain any error message if necessary.
3535

3636
`msg.topic` contains the ip address of the target host.
3737

38-
Default ping is every 20 seconds but can be configured.
38+
There are 2 modes - `Timed` and `Triggered`.
39+
40+
* Timed mode - this is the default mode that pings your devices on a timed basis. Default ping is every 20 seconds but can be configured.
41+
* Triggered mode - this mode permits you to trigger the ping by an input message. If the `Target` is left blank and `msg.payload` is a string or array, you can ping 1 or more devices on demand.
42+
43+
Refer to the built in help on the side-bar info panel for more details.

io/ping/locales/en-US/88-ping.html

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
1-
<script type="text/x-red" data-help-name="ping">
1+
<script type="text/html" data-help-name="ping">
22
<p>Pings a machine and returns the trip time in mS as <code>msg.payload</code>.</p>
3-
<p>Returns <b>false</b> if no response received within 5 seconds, or if the host is unresolveable.</p>
3+
<h3>Output</h3>
4+
<dl class="message-properties">
5+
<dt>payload <span class="property-type">number</span></dt>
6+
<dd> the trip time in mS.</dd>
7+
<dt>topic <span class="property-type">string</span></dt>
8+
<dd> the target host/ip</dd>
9+
<dt>ping <span class="property-type">object</span></dt>
10+
<dd> an object containing <code>host</code> and any other properties sent in the array object. <br>NOTE: This object is only appended when using triggered mode and an array for input payload. It is intended for adavanced users and permits scenarios where you need additional properties to be tagged into your result for use downstream.</dd>
11+
</dl>
12+
<h3>Details</h3>
13+
<p>Returns <b>false</b> if no response received, or if the host is unresolveable.</p>
414
<p>Default ping is every 20 seconds but can be configured.</p>
5-
<p><code>msg.topic</code> contains the target host ip.</p>
15+
16+
<h4>Mode...</h4>
17+
<ul>
18+
<li><b>Timed</b><br>
19+
<P>In <code>Timed</code> mode, the fields <code>Target</code> and <code>Ping (S)</code> must be populated.</P>
20+
<p><code>Target</code> must be a CSV list of hosts / IPs e.g. <code>"192.168.0.1"</code> or <code>"192.168.0.1, www.google.com"</code></p>
21+
<p><code>Ping (S)</code> is the number of seconds between pings</p>
22+
</li>
23+
<li><b>Triggered</b><br>
24+
<p>In <code>Triggered</code> mode, you must connect an input wire and pass a <code>msg</code> in to trigger the ping operation.</p>
25+
<p>If <code>Target</code> is populated, this will be used as the host/ip. The Target must be is a CSV list of hosts / IPs e.g. <code>"192.168.0.1"</code> or <code>"192.168.0.1, www.google.com"</code></p>
26+
27+
<p>If <code>Target</code> is left empty, you can pass a CSV string or an array of hosts in `msg.payload`
28+
<ul>
29+
<li><code>string</code> - a CSV list of hosts / IPs e.g. <code>"192.168.0.1"</code> or <code>"192.168.0.1, www.google.com"</code> </li>
30+
<li><code>array</code> - an array of hosts as string or object. NOTE: The object must contain at minimum <code>.host</code>. Optionally, you can add a <code>timeout</code> property between 1000 & 30000 (default is 5000 / 5 seconds). Additionally, you can add whatever other properties you wish to this object and when the ping result is returned, it will be passed to the next node in <code>msg.ping</code> for use downstream</li>
31+
<li>Example array payload input: <pre>[
32+
"192.168.0.99",
33+
{
34+
"host":"192.168.0.1",
35+
"name":"The router"
36+
},
37+
{
38+
"host":"myapiserver.com",
39+
"name":"external API",
40+
"timeout": 20000,
41+
"support":"[email protected]"
42+
}
43+
]</pre> </li>
44+
</ul>
45+
</p>
46+
</li>
47+
</ul>
48+
49+
50+
651
<p>Note: if running inside Ubuntu Snap you will need to manually start the network-observe interface.
752
`snap connect node-red:network-observe`</p>
853
</script>

io/ping/locales/en-US/88-ping.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
"ping": "ping",
44
"label": {
55
"target": "Target",
6-
"ping": "Ping (S)"
6+
"ping": "Ping (S)",
7+
"mode": "Mode",
8+
"mode_option": {
9+
"timed": "Timed",
10+
"triggered": "Triggered"
11+
}
712
}
813
}
914
}

0 commit comments

Comments
 (0)