Skip to content

Prevent smc and Unbounded from racing during startup - #9020

Open
atavism wants to merge 18 commits into
mainfrom
atavism/smc-status-sync
Open

Prevent smc and Unbounded from racing during startup#9020
atavism wants to merge 18 commits into
mainfrom
atavism/smc-status-sync

Conversation

@atavism

@atavism atavism commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Serialize backend status reconciliation before auto-starting Unbounded so an already-running smc session is detected first, preventing both sharing modes from running simultaneously and keeping the UI synchronized with the actual backend state

atavism and others added 18 commits July 30, 2026 11:53
Co-authored-by: jay <110402935+jay-418@users.noreply.github.com>
The card links out to the web version of Unbounded, but it only ever
rendered when Features[unbounded] is true — which is exactly when the app
already has Unbounded built in, with its own "Unbounded Settings" row
three lines above it. So it offered a download for something the user was
already running, and never appeared in the one case where a pointer
elsewhere might have helped: censored regions, where the server sets the
flag false and the whole block was hidden anyway.

Removed rather than inverted. Showing it when the flag is false would
mean advertising Unbounded precisely where it has been switched off on
purpose.

unboundedAvailable still gates the settings sub-page link, so the
variable stays.
# Conflicts:
#	.github/workflows/release.yml
The peer-status event bus is edge-triggered on the in-process path this
build uses: it carries transitions with no snapshot on subscribe. The peer
client resumes from persisted settings at process start, before any UI is
listening, so the UI never learns sharing is already running and renders
whatever it assumed at startup — the "Configuring network" card that sits
there while Share My Connection is actually serving peers.

Adds PeerStatusJSON() on the core and GetPeerStatus() on the mobile FFI
so the UI can read current state instead of inferring it. Both return ""
on failure rather than a synthesized idle, keeping "not sharing" and
"could not ask" distinguishable.

Bumps radiance for ipc.Client.PeerStatus (getlantern/radiance#617), which
brings lantern-box v0.0.116.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
The share UI is built purely on the peer-status event stream, which is
edge-triggered: transitions only, no snapshot on subscribe. Peer sharing
resumes from persisted settings at process start, well before the UI
exists, so the UI opens at mode=off while SmC is already serving peers.

Toggling on from that state is what produces the stuck card: the UI
writes a setting radiance already has, PatchSettings' diff gate skips
Start, no phase event ever fires, and the card sits on "Configuring
network" indefinitely while sharing works fine underneath.

Adds a getPeerStatus path through every layer that carries one — the
c-archive FFI export (Windows/Linux), the method channel plus its macOS,
iOS and Android handlers, and the service interface — then reconciles
ShareState from it on home mount and starts the event subscription so
later transitions land too.

Only a phase meaning sharing is genuinely up is adopted. idle is the
backend agreeing we are off; error belongs to the toggle path, which
owns the Unbounded fallback. An unreadable status leaves state alone:
"not sharing" and "could not ask" must not render identically.

The _StatusCard idle mapping is deliberately unchanged. After a toggle,
idle really is mid-flight, so remapping it would claim "off" while SmC
serves — the inverse of the bug being fixed.

lantern_generated_bindings.dart carries the single new binding by hand.
A full `make ffigen` on this machine rewrote 8264 lines of unrelated
symbols from local SDK headers, which is toolchain drift, not this
change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
- macOS/iOS: run MobileGetPeerStatus off the main actor. It is an IPC
  round-trip bounded by a 5s Go-side timeout, invoked from a post-frame
  callback at first paint, so evaluating it inside MainActor.run could
  stall rendering. Mirrors the existing probeUPnP detachment. Android was
  already correct — its scope is Dispatchers.IO.
- Drop the redundant error-envelope check in syncFromBackend. peer.Status
  tags Error omitempty, so the key is absent unless the phase is error,
  which the switch already returns on; the requireCore envelope carries no
  phase and is caught by that check alone.
- Normalize the requireCore envelope to "" at the FFI boundary so the
  documented "empty when unreadable" contract holds there too.
- Match the platform-service log message to the method name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
Review asked for coverage of syncFromBackend's parsing and phase gating.
Split the decision out as adoptablePhase so it is reachable without a
WidgetRef, which a ProviderContainer cannot supply, and test it directly:
every phase meaning sharing is up is adopted; empty, malformed, non-object
and phase-less payloads are declined, as are idle, stopping, error, and an
unrecognized future phase. One test pins that declining leaves ShareState
untouched — the "not sharing" vs "could not ask" distinction.

Verified by mutation: adopting idle fails two tests. Dropping the
missing-phase guard fails none, because fromWire maps null to idle, which
the switch already declines — the guard is defensive redundancy, not
distinct behavior, and the comment now says so rather than implying
coverage that cannot exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
The guard sat before the await only. That read is an IPC round-trip
bounded by a 5s timeout and runs at first paint, so the user has a wide
window to hit the toggle while it is in flight.

Adopting the snapshot after that would stamp mode=smc over a session that
had just started as Unbounded, so a later toggle-off would call
setPeerProxy(false) and leave Unbounded running. It would also install a
second event subscription: _startEventSubscription assigns _appEventSub
without cancelling an existing one, so the first leaks and every event is
handled twice.

Found by CodeRabbit on #9017.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
Home can be disposed before the frame settles (a fast route change in the
same frame), and reading a provider from a disposed scope throws. Adds the
if (!context.mounted) return; guard this file already uses on its other
post-frame callbacks, on both the new reconciliation effect and the
adjacent auto-enable one, which had the same gap.

Also moves the reconciliation effect above the "Auto-enable Unbounded"
comment block. It had been inserted between that comment and the effect it
describes, so the comment's "1. App launch (useEffect below)" pointed at
the wrong useEffect.

Found by Copilot on #9017.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LCGZykEHZWRaF5WMraDUcr
@atavism
atavism requested review from myleshorton and a lite review from Copilot and removed request for Copilot August 27, 2026 19:35
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e0b6e85-e029-435f-8ce0-b330a5c7f3fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from fisk/smc-status-sync to main September 2, 2026 16:54
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.

1 participant