[client] Fix two concurrency bugs in the Android session binding

Status() read the run loop's label outside any synchronization and then
stored it, so a login or extend completing in that window was undone by
the stale observation, stranding the UI on "login required" over a
healthy session. Guard the latch with a mutex and a generation counter,
and route both clears through the same helper, so a clear that lands
mid-observation wins.

The watch goroutines kept delivering to removed and replaced listeners:
both subscriptions are buffered (one pending tick, ten pending events),
and unsubscribing only stops new items while the loops drain what is
already queued. On Android those callbacks cross into Java, so a
delivery after teardown can reach a listener whose collaborators are
gone. Give each registration a done signal, close it before
unsubscribing, and check it immediately before every callback.
This commit is contained in:
Zoltán Papp
2026-07-28 17:17:16 +02:00
parent d00068bd89
commit 408301714e
2 changed files with 74 additions and 10 deletions

View File

@@ -8,7 +8,6 @@ import (
"os"
"slices"
"sync"
"sync/atomic"
"time"
"golang.org/x/exp/maps"
@@ -80,10 +79,17 @@ type Client struct {
stateChangeMu sync.Mutex
stateChangeSubID string
eventSub *peer.EventSubscription
// Closed to stop the watch goroutines from delivering buffered items to a
// listener that has been removed or replaced. See stopStateChangeWatchLocked.
stateChangeDone chan struct{}
// Latched "the server wants an interactive login": survives the engine
// restarts that replace the run loop's context state. See Client.Status.
loginRequired atomic.Bool
// Guarded by loginRequiredMu together with loginCleared, which counts
// clears so a stale observation cannot re-latch over one.
loginRequiredMu sync.Mutex
loginRequired bool
loginCleared uint64
extendMu sync.Mutex
extendCancel context.CancelFunc
@@ -162,7 +168,7 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
}
// This path runs the interactive SSO flow, so reaching here means the peer
// is authenticated again — release the latch Status() reports from.
c.loginRequired.Store(false)
c.clearLoginRequired()
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)