fix(meshcore): stop virtual node server during transport teardown#3706
Conversation
) When the real MeshCore node connection drops and the manager enters reconnect state via teardownTransportOnly(), the virtual node server was left running while isConnected() returned false. Any AppStart from a connecting app client got a BadState error, causing the app to loop in "Connecting" indefinitely. Mirror what disconnect() already does: stop the virtual node server before marking the transport as down. The server is restarted in connect() once the real node is back. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBEuSU2sHGZS3xNiXshTxm
|
Claude finished @Yeraze's task —— View job PR Review
SummaryThis is a clean, minimal, and well-targeted fix. The root cause is clear: Code Quality
Potential Issues / Edge Cases1. Reconnect gap vs. virtual node restart When
During the backoff window the virtual node server is down, so any mobile app that tries to connect will get a TCP connection refused rather than a 2. The 3.
Performance / SecurityNo performance or security concerns. This is a state-machine correctness fix with no new I/O paths or privilege changes. Test CoverageThe PR description lists manual test steps but no automated test changes. The existing Verdict✅ Looks good to merge. The fix is correct, minimal, well-commented, and consistent with the existing |
Adds a focused regression test for the PR #3706 fix (issue #3705): when the real node connection drops, teardownTransportOnly() must stop the Virtual Node server. Without this, the VN server kept accepting MeshCore mobile-app connections while isConnected() was false, returning BadState to every AppStart and trapping the app in a "Connecting" loop. The test spies on stopVirtualNodeServer and asserts it is invoked during teardownTransportOnly(), verifies it runs before the native backend teardown (ordering matters — the VN server reads connection state to answer AppStart), and exercises the real stop() delegation. Confirmed to fail without the one-line fix and pass with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review
SummaryThis PR adds 6 lines to fix the root cause (stopping the VN server in Fix (
|
…ter-disconnect (#3705) (#3794) * fix(meshcore): detect socket drops + add user-configurable heartbeat (#3705) Two gaps left a MeshCore source unable to recover from a dropped real-node link without a manual reconnect or container restart: 1. The manager ignored the native backend's own 'disconnected' event (it only logged it), so a socket/serial-level drop left `connected = true`. isConnected() kept returning true, so the Virtual Node server answered AppStart with a stale SelfInfo while real sends silently failed, and nothing recovered. handleUnexpectedDisconnect() now reflects reality (drops to disconnected + stops the VN server) or, when auto-reconnect is enabled, hands off to the existing backoff machinery. An intentionalTeardown guard plus a stale-backend-instance check keep it from firing on our own teardowns. 2. Heartbeat/auto-reconnect was unreachable: neither the source config type nor meshcoreConfigFromSource() carried heartbeatIntervalSeconds, and there was no UI for it — so startHeartbeat() always early-returned and the reconnect path (incl. PR #3706's fix) never ran. Added a heartbeat field to the MeshCore source form (mirroring Meshtastic), plumbed heartbeatIntervalSeconds through MeshCoreSourceConfig and meshcoreConfigFromSource(). The update route already restarts the manager on any config change, so editing it reconnects. Regression tests cover the drop-detection branches and the config passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * fix(meshcore): keep manager registered on manual disconnect (#3705) The UI Disconnect (POST /api/sources/:id/disconnect) called meshcoreManagerRegistry.remove(), deleting the manager from the registry. Every /api/sources/:id/meshcore/* route is behind a guard that 404s with "No MeshCore manager for source <id>" when the manager is absent — so after a manual disconnect the page's status/read polling errored out and the source couldn't be driven again without a container restart. Disconnect now tears down the device link via manager.disconnect() (which already stops the VN server, heartbeat and schedulers) while leaving the manager registered, so /meshcore/* keeps serving a clean disconnected state and /connect re-establishes against the existing manager. Mirrors the intent of the autoConnect=false stop/start workflow without making the source unaddressable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * docs(meshcore): document source heartbeat field + changelog (#3705) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 * fix(meshcore): release dead backend on unrecovered drop (review #3794) Addresses Claude review finding #2: when a socket drop is handled with auto-reconnect disabled, handleUnexpectedDisconnect() left this.nativeBackend pointing at the closed connection, so sendBridgeCommand()'s `!nativeBackend` guard never tripped and callers got a write-to-closed error instead of a clean "disconnected" until /connect ran. Now tears down and nulls the dead backend (connectionState is set to 'disconnected' first so any re-emitted event short-circuits). Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Fixes #3705 — MeshCore virtual node stuck in "Connecting" when the real node connection drops.
teardownTransportOnly()setsisConnected() = falsebut left the virtual node server running. EveryAppStartfrom the mobile app gotBadState→ app looped in "Connecting".await this.stopVirtualNodeServer()at the top ofteardownTransportOnly(), mirroring what the fulldisconnect()path already does. The server restarts inconnect()once the real node is back.Test plan
src/server/meshcoreVirtualNodeServer.test.ts)🤖 Generated with Claude Code
https://claude.ai/code/session_01LBEuSU2sHGZS3xNiXshTxm
Generated by Claude Code