Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lib/features/home/provider/radiance_settings_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ class RadianceSettings extends _$RadianceSettings {

/// Enable/disable the peer-proxy (Share My Connection) radiance
/// setting. Returns the underlying Either so the caller can react to
/// failure — share_my_connection.dart's _start depends on it to
/// revert UI state if the setting flip fails before peer.Client
/// emits its own phase=error StatusEvent. Internal logging still
/// happens on failure for fire-and-forget call sites.
/// failure.
///
/// That Either carries peer.Client.Start's own failure, not merely a
/// failure to flip the setting: radiance runs Start synchronously under
/// the settings patch and propagates its error out. So a caller sees the
/// same failure twice — here and as a phase=error StatusEvent — and must
/// not treat this one as a distinct, earlier class of problem. Internal
/// logging still happens on failure for fire-and-forget call sites.
Future<Either<Failure, Unit>> setPeerProxy(bool value) async {
final svc = ref.read(lanternServiceProvider);
final result = await svc.setPeerProxyEnabled(value);
Expand Down
49 changes: 29 additions & 20 deletions lib/features/share_my_connection/share_my_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -392,32 +392,35 @@ class ShareNotifier extends Notifier<ShareState> {
// bus → core.go listenPeerConnectionEvents → FlutterEvent → our
// Dart subscription.
//
// Failures AFTER peer.Client.Start surface via a phase=error
// StatusEvent that _handlePeerStatus turns into a terminal-
// state reset (mode=off, phase=error). Failures BEFORE
// Start (IPC error, MissingPluginException, core not
// initialized) don't go through that path, so check the
// setPeerProxy Either here and revert UI state to
// mode=off, phase=error directly.
// A failed Start reports itself twice: as a phase=error
// StatusEvent, and as the error setPeerProxy returns, since
// radiance propagates Start's error out through the settings
// patch. Both are handled the same way — fall back to Unbounded
// — and _fallbackToUnbounded ignores whichever arrives second.
final smcRes = await widgetRef
.read(radianceSettingsProvider.notifier)
.setPeerProxy(true);
smcRes.fold(
(err) {
appLogger.error('SmC setPeerProxy failed: ${err.error}');
_stopEventSubscription();
state = ShareState(
active: false,
probing: false,
mode: ShareMode.off,
activeCount: 0,
// Preserve lifetime totalCount across a failed Start — the
// persisted "Total people helped to date" stat is set
// independent of the active session's outcome.
totalCount: state.totalCount,
phase: SharePhase.error,
errorMessage: err.error,
// Falls back rather than reporting. setPeerProxy returns
// peer.Client.Start's own failure, not just the pre-Start errors
// this once assumed, and a router that will not forward a port is
// the most common way it fails. Reporting that as terminal put
// "Couldn't share: ..." on the card while Unbounded was already
// running underneath, so the user saw a failure and a working
// session at once.
//
// radiance rolls PeerShareEnabledKey back before returning, so SmC
// is definitively not running and falling back is all that is left
// to do. Genuine pre-Start failures (IPC down,
// MissingPluginException) land here too and want the same thing:
// the user asked to share, and Unbounded is the way that still
// works. If it does not either, _fallbackToUnbounded surfaces
// that.
Comment thread
myleshorton marked this conversation as resolved.
Outdated
appLogger.error(
'SmC setPeerProxy failed, falling back to Unbounded: ${err.error}',
);
unawaited(_fallbackToUnbounded(widgetRef));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
(_) => null,
);
Expand Down Expand Up @@ -732,6 +735,12 @@ class ShareNotifier extends Notifier<ShareState> {
// the new (Unbounded) mode. _stop is the only teardown path for the
// subscription, and the error path doesn't go through _stop.
Future<void> _fallbackToUnbounded(WidgetRef widgetRef) async {
// One failed Start arrives here twice: from the phase=error event and from
// setPeerProxy's returned error. Without this guard Unbounded is started
// twice and the second call races the first one's state. A plain field
// check suffices — both callers run on the main isolate and the mode flip
// below is synchronous, so whichever arrives second always observes it.
if (state.mode == ShareMode.unbounded) return;
state = ShareState(
active: true,
probing: false,
Expand Down
Loading