From 331b5c8d3fc09416238aa16d7c219b8cc982bd55 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 21 Mar 2026 19:49:05 +0000 Subject: [PATCH] fix(remoteingress edge/hub crash recovery): prevent duplicate crash recovery listeners and reset saved runtime state on shutdown --- changelog.md | 7 +++++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.remoteingressedge.ts | 26 +++++++++++++++++++++++++- ts/classes.remoteingresshub.ts | 6 +++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index b9b6702..41747bd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-21 - 4.14.1 - fix(remoteingress edge/hub crash recovery) +prevent duplicate crash recovery listeners and reset saved runtime state on shutdown + +- Removes existing exit listeners before re-registering crash recovery handlers for edge and hub processes. +- Clears saved edge and hub configuration on stop to avoid stale restart state. +- Resets orphaned edge status intervals and restarts periodic status logging after successful crash recovery. + ## 2026-03-20 - 4.14.0 - feat(quic) add QUIC stability test coverage and bridge logging for hub and edge diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 5098f58..ebd23e7 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/remoteingress', - version: '4.14.0', + version: '4.14.1', description: 'Edge ingress tunnel for DcRouter - tunnels TCP and UDP traffic from the network edge to SmartProxy over TLS or QUIC, preserving client IP via PROXY protocol.' } diff --git a/ts/classes.remoteingressedge.ts b/ts/classes.remoteingressedge.ts index 90a575e..398ccca 100644 --- a/ts/classes.remoteingressedge.ts +++ b/ts/classes.remoteingressedge.ts @@ -139,7 +139,8 @@ export class RemoteIngressEdge extends EventEmitter { throw new Error('Failed to spawn remoteingress-bin'); } - // Register crash recovery handler + // Register crash recovery handler (remove first to avoid duplicates) + this.bridge.removeListener('exit', this.handleCrashRecovery); this.bridge.on('exit', this.handleCrashRecovery); await this.bridge.sendCommand('startEdge', { @@ -189,6 +190,7 @@ export class RemoteIngressEdge extends EventEmitter { this.bridge.kill(); this.started = false; } + this.savedConfig = null; } /** @@ -220,6 +222,12 @@ export class RemoteIngressEdge extends EventEmitter { this.started = false; + // Clear orphaned status interval from previous run + if (this.statusInterval) { + clearInterval(this.statusInterval); + this.statusInterval = undefined; + } + if (this.restartAttempts >= MAX_RESTART_ATTEMPTS) { console.error('[RemoteIngressEdge] Max restart attempts reached, giving up'); this.emit('crashRecoveryFailed'); @@ -237,6 +245,7 @@ export class RemoteIngressEdge extends EventEmitter { return; } + this.bridge.removeListener('exit', this.handleCrashRecovery); this.bridge.on('exit', this.handleCrashRecovery); await this.bridge.sendCommand('startEdge', { @@ -251,6 +260,21 @@ export class RemoteIngressEdge extends EventEmitter { this.started = true; this.restartAttempts = 0; this.restartBackoffMs = 1000; + + // Restart periodic status logging + this.statusInterval = setInterval(async () => { + try { + const status = await this.getStatus(); + console.log( + `[RemoteIngressEdge] Status: connected=${status.connected}, ` + + `streams=${status.activeStreams}, ports=[${status.listenPorts.join(',')}], ` + + `publicIp=${status.publicIp ?? 'unknown'}` + ); + } catch { + // Bridge may be shutting down + } + }, 60_000); + console.log('[RemoteIngressEdge] Successfully recovered from crash'); this.emit('crashRecovered'); } catch (err) { diff --git a/ts/classes.remoteingresshub.ts b/ts/classes.remoteingresshub.ts index 0d2a7b9..d82102b 100644 --- a/ts/classes.remoteingresshub.ts +++ b/ts/classes.remoteingresshub.ts @@ -127,7 +127,8 @@ export class RemoteIngressHub extends EventEmitter { throw new Error('Failed to spawn remoteingress-bin'); } - // Register crash recovery handler + // Register crash recovery handler (remove first to avoid duplicates) + this.bridge.removeListener('exit', this.handleCrashRecovery); this.bridge.on('exit', this.handleCrashRecovery); await this.bridge.sendCommand('startHub', { @@ -158,6 +159,8 @@ export class RemoteIngressHub extends EventEmitter { this.bridge.kill(); this.started = false; } + this.savedConfig = null; + this.savedEdges = []; } /** @@ -214,6 +217,7 @@ export class RemoteIngressHub extends EventEmitter { return; } + this.bridge.removeListener('exit', this.handleCrashRecovery); this.bridge.on('exit', this.handleCrashRecovery); const config = this.savedConfig;