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 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
- - More stable handling of connection losses
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
@@ -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} already existing`);
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
- node.log(`ssh process with pid ${node.sshprocess.pid} started`);
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
- if ( node.statustext !== "remote-access.status.heartbeaterror" ) {
53
- setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
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
- if ( node.statustext !== "remote-access.status.heartbeaterror" ) {
61
- setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
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: ${node.sshprocess.pid}): ${err.name} : ${err.message}`);
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
- node.initialHeartbeatStatus = node.lastHeartbeatStatus; // Wenn es jetzt OK ist, zählt es auch wie initial OK
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 responed a 404...
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 if ( node.lastHeartbeatStatus !== 'OK' ) {
228
- if ( node.initialHeartbeatStatus === 'OK' ) {
229
- // If the status before was ok > Restart communication
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
- killSSHProcess(node)
238
+ // Set serving to false, checkServing will trigger reconnect and startSSH will kill old process
239
+ node.serving = false;
233
240
  } else {
234
- // If the status before had also an error > Just change the label.
235
- setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterroractive"});
236
- node.log(`Heartbeat detected no valid endpoint. Please check your connection settings (Base URL, Serving Port and Protocol).`);
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
- // Kill process
252
- try {
253
- if (node.sshprocess !== undefined) {
254
- node.log(`Killing process ${node.sshprocess.pid}`);
255
- node.sshprocess.removeAllListeners('close');
256
- node.sshprocess.removeAllListeners('exit');
257
- node.sshprocess.removeAllListeners('error');
258
- node.sshprocess.stdout.removeAllListeners('data');
259
- node.sshprocess.stderr.removeAllListeners('data');
260
- node.sshprocess.kill();
261
- node.sshprocess = undefined;
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
- node.serving = false
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.initialHeartbeatStatus = ''
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",
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",