node-red-contrib-remote 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/nodes/remote-access.js +103 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,8 +36,14 @@ You will find more information on [www.remote-red-com](https://www.remote-red.co
|
|
|
36
36
|
|
|
37
37
|
## Version History
|
|
38
38
|
|
|
39
|
+
Version 2.0.5
|
|
40
|
+
- Improved reconnect for Starlink users
|
|
41
|
+
|
|
42
|
+
Version 2.0.4
|
|
43
|
+
- Fix for 'No endpoint detected' message
|
|
44
|
+
|
|
39
45
|
Version 2.0.3
|
|
40
|
-
-
|
|
46
|
+
- More stable handling of connection losses
|
|
41
47
|
|
|
42
48
|
Version 2.0.2
|
|
43
49
|
- Crash on Node-RED startup fixed, extended error reporting
|
package/nodes/remote-access.js
CHANGED
|
@@ -5,18 +5,17 @@ module.exports = function(RED) {
|
|
|
5
5
|
const internalIp = require('internal-ip');
|
|
6
6
|
const instancehashToAccessNode = {};
|
|
7
7
|
|
|
8
|
-
function startSSH(node, server, port) {
|
|
8
|
+
async function startSSH(node, server, port) {
|
|
9
9
|
try {
|
|
10
10
|
node.log("starting ssh process");
|
|
11
11
|
|
|
12
12
|
// Is there a old node.sshprocess object?
|
|
13
13
|
if (node.sshprocess !== undefined) {
|
|
14
|
-
node.log(`ssh process with pid ${node.sshprocess.pid}
|
|
15
|
-
killSSHProcess(node);
|
|
14
|
+
node.log(`checking old ssh process with pid ${node.sshprocess.pid}`);
|
|
15
|
+
await killSSHProcess(node);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Reset heartbeat status
|
|
19
|
-
node.initialHeartbeatStatus = ''
|
|
18
|
+
// Reset heartbeat status for current tunnel
|
|
20
19
|
node.lastHeartbeatStatus = ''
|
|
21
20
|
|
|
22
21
|
// Create ssh command
|
|
@@ -30,7 +29,8 @@ module.exports = function(RED) {
|
|
|
30
29
|
sshparameters.push('-v');
|
|
31
30
|
}
|
|
32
31
|
node.sshprocess = child_process.spawn("ssh", sshparameters);
|
|
33
|
-
|
|
32
|
+
const currentPid = node.sshprocess.pid;
|
|
33
|
+
node.log(`ssh process with pid ${currentPid} started`);
|
|
34
34
|
|
|
35
35
|
// Set serving.. if not working, the process will exit or close
|
|
36
36
|
setStatus(node, {fill:"green",shape:"dot",text:"remote-access.status.serving"});
|
|
@@ -49,23 +49,29 @@ module.exports = function(RED) {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
node.sshprocess.on('close', (code, signal) => {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
node.log(`ssh process close (pid: ${currentPid} code: ${code} signal: ${signal})`);
|
|
53
|
+
// Only update status if this is still the current process
|
|
54
|
+
if (node.sshprocess && node.sshprocess.pid === currentPid) {
|
|
55
|
+
if ( node.statustext !== "remote-access.status.heartbeaterror" ) {
|
|
56
|
+
setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
|
|
57
|
+
}
|
|
58
|
+
node.serving = false
|
|
54
59
|
}
|
|
55
|
-
node.serving = false
|
|
56
|
-
node.log(`ssh process close (pid: ${node.sshprocess.pid} code: ${code} signal: ${signal})`);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
node.sshprocess.on('exit', (code, signal) => {
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
node.log(`ssh process exit (pid: ${currentPid} code: ${code} signal: ${signal})`);
|
|
64
|
+
// Only update status if this is still the current process
|
|
65
|
+
if (node.sshprocess && node.sshprocess.pid === currentPid) {
|
|
66
|
+
if ( node.statustext !== "remote-access.status.heartbeaterror" ) {
|
|
67
|
+
setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
|
|
68
|
+
}
|
|
69
|
+
node.serving = false
|
|
62
70
|
}
|
|
63
|
-
node.serving = false
|
|
64
|
-
node.log(`ssh process exit (pid: ${node.sshprocess.pid} code: ${code} signal: ${signal})`);
|
|
65
71
|
});
|
|
66
72
|
|
|
67
73
|
node.sshprocess.on('error', (err) => {
|
|
68
|
-
node.log(`ssh process error (pid: ${
|
|
74
|
+
node.log(`ssh process error (pid: ${currentPid}): ${err.name} : ${err.message}`);
|
|
69
75
|
});
|
|
70
76
|
} catch (e) {
|
|
71
77
|
// TODO: Error: socket hang up
|
|
@@ -211,29 +217,39 @@ module.exports = function(RED) {
|
|
|
211
217
|
})
|
|
212
218
|
.then(response => {
|
|
213
219
|
// Remember the status
|
|
214
|
-
if ( node.initialHeartbeatStatus === '' ) {
|
|
215
|
-
node.initialHeartbeatStatus = response.data.status
|
|
216
|
-
}
|
|
217
220
|
node.lastHeartbeatStatus = response.data.status
|
|
218
221
|
|
|
219
222
|
// Based on the status...
|
|
220
223
|
if ( node.lastHeartbeatStatus === 'OK' ) {
|
|
221
|
-
|
|
224
|
+
// Successful heartbeat
|
|
225
|
+
node.configurationValidated = true; // Configuration is definitely OK
|
|
226
|
+
node.initialRetryCount = 0; // Reset retry counter on success
|
|
222
227
|
setStatus(node, {fill:"green",shape:"dot",text:"remote-access.status.serving"});
|
|
223
228
|
} else if ( node.lastHeartbeatStatus === 'NOTFOUND' ) {
|
|
224
|
-
// The local endpoint
|
|
229
|
+
// The local endpoint responded with a 404...
|
|
225
230
|
setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterrornotfound"});
|
|
226
231
|
node.log(`Heartbeat detected no valid endpoint, got a 404 response. Please check the base URL in the connection settings.`);
|
|
227
|
-
} else
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
} else {
|
|
233
|
+
// Heartbeat error - decide whether to reconnect
|
|
234
|
+
if ( node.configurationValidated === true ) {
|
|
235
|
+
// Configuration was validated before → Reconnect
|
|
230
236
|
setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.heartbeaterror"});
|
|
231
237
|
node.log(`Heartbeat error. Reconnecting soon.`);
|
|
232
|
-
|
|
238
|
+
// Set serving to false, checkServing will trigger reconnect and startSSH will kill old process
|
|
239
|
+
node.serving = false;
|
|
233
240
|
} else {
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
// Never worked yet - check if still in startup phase
|
|
242
|
+
if ( node.initialRetryCount < 3 ) {
|
|
243
|
+
// Still in startup phase - give it more chances (after reboot with slow network, Starlink reconnect, etc.)
|
|
244
|
+
node.initialRetryCount++;
|
|
245
|
+
setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterror"});
|
|
246
|
+
node.log(`Heartbeat error during startup (attempt ${node.initialRetryCount}/3). Reconnecting soon.`);
|
|
247
|
+
node.serving = false; // Trigger reconnect
|
|
248
|
+
} else {
|
|
249
|
+
// Too many failed attempts > Misconfiguration > NO reconnect
|
|
250
|
+
setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterroractive"});
|
|
251
|
+
node.log(`Heartbeat detected no valid endpoint. Please check your connection settings (Base URL, Serving Port and Protocol).`);
|
|
252
|
+
}
|
|
237
253
|
}
|
|
238
254
|
}
|
|
239
255
|
})
|
|
@@ -248,22 +264,62 @@ module.exports = function(RED) {
|
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
function killSSHProcess(node) {
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
node.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
// Returns a promise that kills the current process. Resolves when process is dead or after 2 seconds.
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
try {
|
|
270
|
+
if (node.sshprocess !== undefined) {
|
|
271
|
+
const pidToKill = node.sshprocess.pid;
|
|
272
|
+
const processToKill = node.sshprocess;
|
|
273
|
+
|
|
274
|
+
// Remove existing listeners
|
|
275
|
+
processToKill.removeAllListeners('close');
|
|
276
|
+
processToKill.removeAllListeners('exit');
|
|
277
|
+
processToKill.removeAllListeners('error');
|
|
278
|
+
processToKill.stdout.removeAllListeners('data');
|
|
279
|
+
processToKill.stderr.removeAllListeners('data');
|
|
280
|
+
|
|
281
|
+
// Check if process is already dead (exitCode is set when process has exited)
|
|
282
|
+
if (processToKill.exitCode !== null || processToKill.killed) {
|
|
283
|
+
node.log(`Process ${pidToKill} already dead (exitCode: ${processToKill.exitCode}, killed: ${processToKill.killed})`);
|
|
284
|
+
node.sshprocess = undefined;
|
|
285
|
+
node.serving = false;
|
|
286
|
+
resolve();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
node.log(`Killing process ${pidToKill}`);
|
|
291
|
+
|
|
292
|
+
// Safety timeout: resolve after 2 seconds even if exit event doesn't fire
|
|
293
|
+
const safetyTimeout = setTimeout(() => {
|
|
294
|
+
node.log(`Process ${pidToKill} kill timeout after 2 seconds`);
|
|
295
|
+
node.serving = false;
|
|
296
|
+
resolve();
|
|
297
|
+
}, 2000);
|
|
298
|
+
|
|
299
|
+
// Set up one-time exit handler to know when process is really dead
|
|
300
|
+
const exitHandler = () => {
|
|
301
|
+
clearTimeout(safetyTimeout);
|
|
302
|
+
node.log(`Process ${pidToKill} confirmed killed`);
|
|
303
|
+
node.serving = false;
|
|
304
|
+
resolve();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Add one-time exit handler
|
|
308
|
+
processToKill.once('exit', exitHandler);
|
|
309
|
+
|
|
310
|
+
// Send kill signal
|
|
311
|
+
processToKill.kill();
|
|
312
|
+
node.sshprocess = undefined;
|
|
313
|
+
} else {
|
|
314
|
+
node.serving = false;
|
|
315
|
+
resolve();
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
node.error(`Error in killSSHProcess: ${error}`);
|
|
319
|
+
node.serving = false;
|
|
320
|
+
resolve();
|
|
262
321
|
}
|
|
263
|
-
|
|
264
|
-
} catch (error) {
|
|
265
|
-
node.error(`Error in killSSHProcess: ${error}`);
|
|
266
|
-
}
|
|
322
|
+
});
|
|
267
323
|
}
|
|
268
324
|
|
|
269
325
|
function RemoteAccessNode(config) {
|
|
@@ -311,8 +367,9 @@ module.exports = function(RED) {
|
|
|
311
367
|
}
|
|
312
368
|
|
|
313
369
|
// Init heartbeat
|
|
314
|
-
node.
|
|
315
|
-
node.lastHeartbeatStatus = ''
|
|
370
|
+
node.configurationValidated = false; // Was the configuration EVER successfully validated?
|
|
371
|
+
node.lastHeartbeatStatus = '';
|
|
372
|
+
node.initialRetryCount = 0; // Counts retry attempts during startup phase
|
|
316
373
|
node.heartbeatinterval = setInterval(heartbeat, 5*60*1000, node);
|
|
317
374
|
|
|
318
375
|
// Call API for announce the instacehash and authentication, retrive server and port.
|
|
@@ -376,8 +433,8 @@ module.exports = function(RED) {
|
|
|
376
433
|
// Set status
|
|
377
434
|
setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopping"});
|
|
378
435
|
|
|
379
|
-
// Kill process
|
|
380
|
-
killSSHProcess(node)
|
|
436
|
+
// Kill process (don't wait as Node-RED has a timeout for close handlers)
|
|
437
|
+
killSSHProcess(node).catch(err => node.error(`Error killing SSH process on close: ${err}`))
|
|
381
438
|
|
|
382
439
|
// Cancel timeouts
|
|
383
440
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-remote",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Remote-RED is an ecosystem to bring remote access, push notification and geofencing to Node-RED. There are also additional functions like directly answer on a notification and homescreen widgets.",
|
|
5
5
|
"author": "Thorsten Heilmann",
|
|
6
6
|
"license": "GPL-3.0",
|