Wire peer.Client into LocalBackend (Share My Connection PR 2/4)#460
Open
myleshorton wants to merge 3 commits into
Open
Wire peer.Client into LocalBackend (Share My Connection PR 2/4)#460myleshorton wants to merge 3 commits into
myleshorton wants to merge 3 commits into
Conversation
Contributor
There was a problem hiding this comment.
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
PeerShareEnabledKeyand backend logic to start, stop, resume, and report peer-share state. - Emitted peer lifecycle status events from
peer.Clientand 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.
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>
6a238ce to
8c31c0a
Compare
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>
8c31c0a to
d5a0750
Compare
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>
This was referenced May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on #458 (peer module + portforward). Targets
fisk/peer-module; rebase ontomainwhen #458 merges.What's wired up
common/settings:PeerShareEnabledKey(bool).peer.Client: emitspeer.StatusEventon Start success and Stop completion.backend.LocalBackend:peerControllerinterface seam (so tests can inject a fake)NewLocalBackendwithkindling.HTTPClient()+common.GetBaseURL()+ the device IDapplyPeerSharehelper, called fromPatchSettingsdispatchPeerStatus()accessor for the IPC handleripc:GET /peer/status(snapshot) +GET /peer/status/events(SSE).Three subtle behaviors worth flagging
peerToggleMu. Without it, a fastoff→on→offcould see the second call's"already active"rollback racing the third call'sStop, and the persisted setting could end up disagreeing with runtime state.peerWG. If the user left the toggle on across restarts,LocalBackend.Start()resumes the session in a goroutine. Without the WG, aClose()racing the resume could cancelr.ctxmid-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.Stoperrors 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/_StartFailureRollsBackSettingTestResumePeerShare_NoopWhenSettingOff/_StartsWhenSettingOnTestClose_WaitsForResumeAndStopsActivePeer— exercises the WG race using aslowStartFakethat blocks Start on a gate channel; asserts Close cannot return until the gate is releasedTestPatchSettings_PeerShareDispatches— PatchSettings({PeerShareEnabledKey: true}) → fake.startCalls == 1 (catches a typo'd dispatch key)TestClient_StatusEventEmittedOnStartAndStopinpeer/— Subscribe and assert both edges fire with the expectedStatusgo test -race ./peer/... ./backend/... ./common/settings/... ./ipc/...andgolangci-lint run --new-from-rev=origin/mainare both clean.Out of scope (PR 3-4)
setPeerProxyEnabledFFI export + ffigen regen.setPeerProxyrewrite mirroringsetBlockAds, 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🤖 Generated with Claude Code