Skip to content

Wire peer.Client into LocalBackend (Share My Connection PR 2/4)#460

Open
myleshorton wants to merge 3 commits into
fisk/peer-modulefrom
fisk/peer-localbackend
Open

Wire peer.Client into LocalBackend (Share My Connection PR 2/4)#460
myleshorton wants to merge 3 commits into
fisk/peer-modulefrom
fisk/peer-localbackend

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

Stacks on #458 (peer module + portforward). Targets fisk/peer-module; rebase onto main when #458 merges.

What's wired up

Dart toggle → FFI (PR 3) → backend.PatchSettings({PeerShareEnabledKey: true})
                                                          │
                                                          ▼
                                                  applyPeerShare(true)
                                                          │
                          ┌───────────────────────────────┼────────────────────────┐
                          ▼                               ▼                        ▼
                  peer.Client.Start             rollback setting            emit peer.StatusEvent
                   (UPnP → register             on Start failure           (subscribed by IPC SSE)
                    → sing-box → hb)
  • common/settings: PeerShareEnabledKey (bool).
  • peer.Client: emits peer.StatusEvent on Start success and Stop completion.
  • backend.LocalBackend:
    • new peerController interface seam (so tests can inject a fake)
    • constructed in NewLocalBackend with kindling.HTTPClient() + common.GetBaseURL() + the device ID
    • new applyPeerShare helper, called from PatchSettings dispatch
    • new PeerStatus() accessor for the IPC handler
  • ipc: GET /peer/status (snapshot) + GET /peer/status/events (SSE).

Three subtle behaviors worth flagging

  1. Toggle calls are serialized by peerToggleMu. Without it, a fast off→on→off could see the second call's "already active" rollback racing the third call's Stop, and the persisted setting could end up disagreeing with runtime state.
  2. Start runs under a 30s deadline. UPnP discovery + register + box build is normally single-digit seconds; the deadline caps the worst case so a hung router can't block the IPC toggle response indefinitely.
  3. Auto-resume + Close synchronize via peerWG. If the user left the toggle on across restarts, LocalBackend.Start() resumes the session in a goroutine. Without the WG, a Close() racing the resume could cancel r.ctx mid-Start, leaving a registered route + open box behind on the cloud side.

Rollback semantics

Mirroring setBlockAds: write the setting, try the side effect, on failure roll the setting back AND return the error so the Dart toggle can re-render to "off" with a user-facing error. Stop errors are logged but not surfaced — the user wants the session off, and a partial teardown shouldn't keep the toggle on.

Test coverage

  • TestApplyPeerShare_StartsOnEnable / _StopsOnDisable / _StartFailureRollsBackSetting
  • TestResumePeerShare_NoopWhenSettingOff / _StartsWhenSettingOn
  • TestClose_WaitsForResumeAndStopsActivePeer — exercises the WG race using a slowStartFake that blocks Start on a gate channel; asserts Close cannot return until the gate is released
  • TestPatchSettings_PeerShareDispatches — PatchSettings({PeerShareEnabledKey: true}) → fake.startCalls == 1 (catches a typo'd dispatch key)
  • TestClient_StatusEventEmittedOnStartAndStop in peer/ — Subscribe and assert both edges fire with the expected Status

go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... and golangci-lint run --new-from-rev=origin/main are both clean.

Out of scope (PR 3-4)

  • PR 3 (lantern-core): setPeerProxyEnabled FFI export + ffigen regen.
  • PR 4 (lantern): Dart setPeerProxy rewrite mirroring setBlockAds, optional status indicator using the new SSE.

Test plan

  • go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/...
  • golangci-lint run --new-from-rev=origin/main
  • Manual end-to-end deferred until PR 4 (when the Dart toggle lands)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 4, 2026 22:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires the new peer.Client into LocalBackend so the persisted “Share My Connection” setting can start/stop the peer session and expose its state over IPC. It fits the second step of the stacked peer-share rollout by connecting the already-built peer module to backend settings and status reporting.

Changes:

  • Added PeerShareEnabledKey and backend logic to start, stop, resume, and report peer-share state.
  • Emitted peer lifecycle status events from peer.Client and exposed snapshot/SSE IPC handlers for that state.
  • Added backend and peer tests covering enable/disable, resume-on-start, shutdown coordination, and status-event emission.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
peer/peer.go Emits peer status events on successful start and stop.
peer/peer_test.go Adds tests for peer status event emission.
ipc/server.go Adds /peer/status and /peer/status/events IPC endpoints.
common/settings/settings.go Introduces persisted peer_share_enabled setting key.
backend/radiance.go Wires peer client construction, toggle handling, auto-resume, shutdown, and status accessors into LocalBackend.
backend/radiance_test.go Adds tests for peer-share apply/resume/close/dispatch behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/radiance.go Outdated
Comment thread peer/peer.go
myleshorton pushed a commit that referenced this pull request May 5, 2026
Two fixes from review on #460.

1) applyPeerShare's disable branch was calling peer.Stop with r.ctx
   (no deadline). Deregister or UnmapPort stalling on a slow gateway
   would hang the IPC /settings PATCH and leave the UI toggle without
   a response. Wrap both branches in a single peerToggleTimeout-bounded
   context (renamed from peerStartTimeout to reflect that it now
   covers both directions).

2) The SSE handler streamed the StatusEvent's captured snapshot, but
   events.Emit dispatches each subscriber callback in its own goroutine
   so a quick start→stop pair could land in the channel out of order
   — the consumer would briefly see "active" *after* "inactive".
   Reworked the handler to use the event purely as a wake-up trigger
   and read the live snapshot from PeerStatus() before each send.
   Out-of-order trigger goroutines now just produce duplicate reads
   of the same final state instead of stale-state flicker.

go test -race ./peer/... ./backend/... ./ipc/... and golangci-lint
both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton pushed a commit that referenced this pull request May 5, 2026
Two fixes from review on #460.

1) applyPeerShare's disable branch was calling peer.Stop with r.ctx
   (no deadline). Deregister or UnmapPort stalling on a slow gateway
   would hang the IPC /settings PATCH and leave the UI toggle without
   a response. Wrap both branches in a single peerToggleTimeout-bounded
   context (renamed from peerStartTimeout to reflect that it now
   covers both directions).

2) The SSE handler streamed the StatusEvent's captured snapshot, but
   events.Emit dispatches each subscriber callback in its own goroutine
   so a quick start→stop pair could land in the channel out of order
   — the consumer would briefly see "active" *after* "inactive".
   Reworked the handler to use the event purely as a wake-up trigger
   and read the live snapshot from PeerStatus() before each send.
   Out-of-order trigger goroutines now just produce duplicate reads
   of the same final state instead of stale-state flicker.

go test -race ./peer/... ./backend/... ./ipc/... and golangci-lint
both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton force-pushed the fisk/peer-localbackend branch from 6a238ce to 8c31c0a Compare May 5, 2026 16:26
Adam Fisk and others added 2 commits May 5, 2026 16:56
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458).

* common/settings: add PeerShareEnabledKey bool.
* peer.Client: emit StatusEvent on Start success and Stop completion so
  subscribers (the new IPC SSE handler) can drive UI without polling.
* backend.LocalBackend: own a peerController (interface seam over
  peer.Client) constructed in NewLocalBackend with kindling's HTTP
  client + the lantern-cloud base URL + the device ID.
* PatchSettings dispatch: PeerShareEnabledKey changes route to
  applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu
  so a fast off→on→off can't see the second call's "already active"
  rollback racing the third call's Stop. Start runs against a 30s
  deadline so a slow router can't block the IPC response indefinitely.
  On Start failure the persisted setting is rolled back so reads of
  PeerShareEnabledKey reflect runtime state and the Dart toggle can
  surface the error.
* Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(),
  kick off Start in a goroutine tracked by peerWG. Close() waits for
  peerWG before tearing down ctx, so an in-flight resume can't leave
  a registered route + open box behind on shutdown.
* Close: if peerClient.IsActive() after the WG settles, Stop with a
  fresh context so Deregister has a live HTTP deadline.
* IPC: new GET /peer/status (snapshot) and GET /peer/status/events
  (SSE). The SSE handler replays the current snapshot on connect.

Tests cover applyPeerShare's three branches (enable, disable, Start
failure rolls back the setting), the resume-if-enabled path, the
Close-waits-for-resume + Stop-active-peer race, and the PatchSettings
dispatch wiring (a typo on the diff key would silently break the
toggle without it). peer_test adds a Subscribe-and-assert test for
StatusEvent emission on both edges.

go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/...
golangci-lint run --new-from-rev=origin/main both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes from review on #460.

1) applyPeerShare's disable branch was calling peer.Stop with r.ctx
   (no deadline). Deregister or UnmapPort stalling on a slow gateway
   would hang the IPC /settings PATCH and leave the UI toggle without
   a response. Wrap both branches in a single peerToggleTimeout-bounded
   context (renamed from peerStartTimeout to reflect that it now
   covers both directions).

2) The SSE handler streamed the StatusEvent's captured snapshot, but
   events.Emit dispatches each subscriber callback in its own goroutine
   so a quick start→stop pair could land in the channel out of order
   — the consumer would briefly see "active" *after* "inactive".
   Reworked the handler to use the event purely as a wake-up trigger
   and read the live snapshot from PeerStatus() before each send.
   Out-of-order trigger goroutines now just produce duplicate reads
   of the same final state instead of stale-state flicker.

go test -race ./peer/... ./backend/... ./ipc/... and golangci-lint
both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton force-pushed the fisk/peer-localbackend branch from 8c31c0a to d5a0750 Compare May 5, 2026 22:56
Two changes to make the failure-on-discover path actually surface its
underlying error instead of crashing the IPC handler.

  1. peer/peer.go's default cfg.NewForwarder wrapped
     portforward.NewForwarder with a bare `return portforward.NewForwarder(ctx)`.
     When discovery failed, that collapsed the `(*Forwarder)(nil), err`
     pair into a typed-nil interface — `if fwd != nil` in the deferred
     cleanup passed (the interface has a type), `fwd.UnmapPort(...)`
     dispatched to a nil receiver, and `f.mu.Lock()` panicked. The
     wrapper now returns a clean `nil, err` so the caller sees
     ErrNoPortForwarding (or whatever the discoverer returned) and the
     deferred cleanup short-circuits on the interface nil-check.

  2. portforward.UnmapPort grew a defensive `if f == nil { return nil }`
     at the top. Belt-and-suspenders for any future caller that lands
     here through an interface and bypasses the inline nil-check —
     teardown should be idempotent on a nil receiver, not a panic.

Reproduced live on macOS 26.x with `Share My Connection` toggled on
when UPnP discovery returned ErrNoPortForwarding; the http2 IPC
goroutine panicked instead of rolling back the toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants