Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
cee9190
fix(ios): adopt the UIScene life cycle so Xcode 27 builds launch
chrisnojima Sep 16, 2026
431ef43
fix(ios): report foreground on become-active so the http server start…
chrisnojima Sep 16, 2026
eebd580
refactor(ios): follow Expo's scene template
chrisnojima Sep 16, 2026
6c14909
fix(appstate): add a state generation, iOS background start, and chea…
chrisnojima Sep 16, 2026
354cbec
fix(leveldb): flush by rotating the memtable and coalesce concurrent …
chrisnojima Sep 16, 2026
77184f1
fix(appstate): owners undo only their own app-state transitions
chrisnojima Sep 16, 2026
21bd13c
fix(appstate): record background task windows monotonically
chrisnojima Sep 16, 2026
0201f9a
refactor(appstate): drive app state from lifecycle events in a testab…
chrisnojima Sep 16, 2026
b4982d8
test(appstate): cover background task window guards and sync the reco…
chrisnojima Sep 16, 2026
c68b7f9
fix(kbhttp): restart dead servers, keep one token, and tear down only…
chrisnojima Sep 16, 2026
05db752
fix(kbhttp): restart after unexpected Serve exits, follow transitions…
chrisnojima Sep 16, 2026
6c50bbe
fix(kbhttp): decide and restart after an unexpected exit under the ma…
chrisnojima Sep 16, 2026
951ebe9
fix(gregor): gate connects on app state and keep the connection up in…
chrisnojima Sep 16, 2026
cddc642
fix(gregor): keep gregor down after logout and replace a stale connec…
chrisnojima Sep 16, 2026
076bd96
fix(gregor): install a gregor client only for the current connection
chrisnojima Sep 16, 2026
b804cfc
fix(gregor): keep OnConnect's post-sync steps from outliving their co…
chrisnojima Sep 16, 2026
fbc7553
fix(chat): keep background work consistent with app state across rest…
chrisnojima Sep 16, 2026
70d0d2a
fix(chat): close remaining app-state races in the indexer, archive an…
chrisnojima Sep 16, 2026
4bf2220
fix(appstate): restart the leveldb cleaner monitor on reopen and stop…
chrisnojima Sep 16, 2026
987ef67
fix(kbfs): end paused background loops on shutdown and keep a cellula…
chrisnojima Sep 16, 2026
64f7f41
fix(kbfs): run the local HTTP server in every app state but BACKGROUND
chrisnojima Sep 16, 2026
cc5c5c5
fix(kbfs): keep the local HTTP server up in every app state on Android
chrisnojima Sep 16, 2026
0ecf32e
fix(ios): forward lifecycle events to Go off the main thread and deli…
chrisnojima Sep 16, 2026
f7e5d35
fix(appstate): apply termination before the pending-message warning a…
chrisnojima Sep 16, 2026
c2f21d9
fix(mobile): keep JS app state, http server address and push navigati…
chrisnojima Sep 16, 2026
f3f6d30
fix(mobile): take the iOS app state from scene notifications and keep…
chrisnojima Sep 16, 2026
d69d76f
feat(ios): run live location natively so it works without JS
chrisnojima Sep 16, 2026
cebf667
fix(ios): report the first live location fix at once and test the Loc…
chrisnojima Sep 16, 2026
553a0ef
fix(chat): give up on the chat UI location watch after its retry limit
chrisnojima Sep 16, 2026
9eee1d5
fix(android): report the process lifecycle and push windows to Go as …
chrisnojima Sep 16, 2026
1c44ba2
fix(android): background on full-screen pickers, drop the unused push…
chrisnojima Sep 16, 2026
4c6cf81
fix(android): send quick replies in the foreground and clear duplicat…
chrisnojima Sep 16, 2026
6d730c4
fix(mobile): report quick reply send failures and run the reply off t…
chrisnojima Sep 16, 2026
97e83b7
test(android): bound the receiver work tests so a caller-thread regre…
chrisnojima Sep 16, 2026
f5f6d59
test(ios): add simulator lifecycle e2e flows for app state, deep link…
chrisnojima Sep 17, 2026
a472d1f
test(ios): let the Appium harness start sessions under Xcode 27
chrisnojima Sep 17, 2026
5d4af14
test(ios): harden the lifecycle e2e flows and type-check the e2e suit…
chrisnojima Sep 17, 2026
df59108
test(ios): use headless Appium sessions only where Simulator.app is m…
chrisnojima Sep 17, 2026
88365d8
fix(push): open the conversation from a cold-start tap on a push for …
chrisnojima Sep 17, 2026
cd21869
test(ios): wait for the native conversation push before openSelfConve…
chrisnojima Sep 17, 2026
8168ea7
fix(ios): append startup timing lines to ios.log instead of replacing…
chrisnojima Sep 17, 2026
10b5537
refactor(appstate): derive the app state from the UI state and backgr…
chrisnojima Sep 17, 2026
b684c6c
fix(appstate): tie the live location hold to the tracker map and remo…
chrisnojima Sep 17, 2026
5dc6692
refactor(kbhttp): run the local http server from a single reconciling…
chrisnojima Sep 17, 2026
83df128
refactor(kbhttp): run the kbfs and kbhttp local servers from one shar…
chrisnojima Sep 17, 2026
d94e71c
fix(kbhttp): name each local server in its log lines and keep the sta…
chrisnojima Sep 17, 2026
3b137dc
refactor(lifecycle): let Go own the background task and take stay as …
chrisnojima Sep 17, 2026
8eda152
refactor(service): drop the UpdateAppState handler
chrisnojima Sep 17, 2026
014db21
test(appstate): table the keygen transition and drop the CAS-era stre…
chrisnojima Sep 17, 2026
6e3418e
fix(lifecycle): close the controller on shutdown and read the retry c…
chrisnojima Sep 17, 2026
2041035
fix(gregor): serialize the connect tail with shutdown on one mutex
chrisnojima Sep 17, 2026
4a8fe5c
test(gregor): widen the tail stress window and report its deadlock guard
chrisnojima Sep 17, 2026
0e5c4b1
test(gregor): let the gate stress test report its own deadlock guard
chrisnojima Sep 17, 2026
2dae407
refactor(push): hand tapped pushes to JS through a native tap slot an…
chrisnojima Sep 17, 2026
5923dde
chore(android): drop dead imports from the push files
chrisnojima Sep 17, 2026
4b524f2
fix(push): test the tap intent's identity and cover the devices link …
chrisnojima Sep 17, 2026
8ad9fd6
feat(protocol): version the bootstrap status and the session and http…
chrisnojima Sep 17, 2026
f35c43e
fix(startup): subscribe before the bootstrap read and apply whichever…
chrisnojima Sep 17, 2026
c6fb49c
fix(startup): keep the reconnect synchronous and stop a superseded re…
chrisnojima Sep 17, 2026
cb21f26
fix(mobile): drop the legacy expo location task and prove JS never st…
chrisnojima Sep 17, 2026
20a889d
test(mobile): cover the task-registered guard and control both metro …
chrisnojima Sep 17, 2026
aedc3d0
fix(deeplinks): send keybase://devices to the platform's devices screen
chrisnojima Sep 17, 2026
da40771
fix(daemon): start the handshake even if the ordering promise rejects
chrisnojima Sep 17, 2026
9056238
fix(kbhttp): publish an empty status before the server can be observed
chrisnojima Sep 17, 2026
13cdd0f
docs: record why the gate, the link split and the initial URL work as…
chrisnojima Sep 17, 2026
95a7e9a
refactor(appstate): one shared watcher for the four app-state monitors
chrisnojima Sep 18, 2026
a3cf5b7
refactor(lifecycle): let a background task's lifetime ride on its hold
chrisnojima Sep 18, 2026
6ea3add
refactor(kbhttp): New stops returning an error
chrisnojima Sep 18, 2026
a0f3aae
refactor(service): take the gregor gate's mutex in onGateIfCurrent
chrisnojima Sep 18, 2026
f8813fe
fix(livelocation): re-acquire a hold the controller ended
chrisnojima Sep 18, 2026
8caa18a
fix(kbhttp): keep kbfs's failed first bind fatal
chrisnojima Sep 18, 2026
a6dd9a3
test(lifecycle): pin that no background task starts after Close
chrisnojima Sep 18, 2026
e45a49a
docs(service): re-point the gate comment at onGateIfCurrent
chrisnojima Sep 18, 2026
93831ea
feat(protocol): return the client state from setNotifications
chrisnojima Sep 18, 2026
51b5784
refactor(config): take the session from the subscription, not a read
chrisnojima Sep 18, 2026
0da5ca2
fix(config): order the bootstrap status's identity against the session
chrisnojima Sep 18, 2026
54a10b5
fix(protocol): say "session not known yet" instead of guessing logged…
chrisnojima Sep 18, 2026
6c7a106
fix(config): hand the session back to versioned ordering once it settles
chrisnojima Sep 18, 2026
5e8d0a0
fix(daemon): keep the handshake generation across a store reset
chrisnojima Sep 18, 2026
765b30c
feat(protocol): announce the app's lifecycle state to clients
chrisnojima Sep 18, 2026
197e6c4
refactor(app): take the app state from the service, not from the OS t…
chrisnojima Sep 18, 2026
87f21b9
fix(appstate): stamp the app state's version under the lock that wrot…
chrisnojima Sep 18, 2026
a7a9849
feat(protocol): hand a resolved push-tap route to clients
chrisnojima Sep 18, 2026
689f89b
feat(service): resolve a tapped notification to a route in the service
chrisnojima Sep 18, 2026
41b6266
refactor(push): take the tap route from the service
chrisnojima Sep 18, 2026
0ffc8d8
fix(push): retire a tap when the client says it acted, not when it is…
chrisnojima Sep 18, 2026
4a42951
fix(push): mark a tap queued only once the queue has taken it
chrisnojima Sep 18, 2026
863e40c
refactor(native): delete the dead engineReset verb
chrisnojima Sep 18, 2026
04119d4
refactor(native): drop dead branches and fields in the push path
chrisnojima Sep 18, 2026
13aeffe
refactor(android): flush a share intent when JS asks, not on a poll
chrisnojima Sep 18, 2026
2c5eea6
fix(settings): keep a notification that lands during the settings load
chrisnojima Sep 18, 2026
b668e10
fix(links): route /phone-app through the linking config
chrisnojima Sep 18, 2026
72bfef8
refactor(js): collapse the no-behaviour leftovers from the simplifica…
chrisnojima Sep 18, 2026
195157c
fix(links): carry an invite launch link across the signup it triggers
chrisnojima Sep 18, 2026
f67b8d6
fix(settings): drop a settings reply that lands after a logout
chrisnojima Sep 18, 2026
851945e
refactor(service): make setNotifications' register-before-read a comp…
chrisnojima Sep 18, 2026
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
51 changes: 51 additions & 0 deletions go/avatars/appstate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package avatars

import (
"sync"

"github.com/keybase/client/go/libkb"
"github.com/keybase/client/go/protocol/keybase1"
)

// backgroundFlusher runs flush each time the app enters BACKGROUND, from
// start until stop.
type backgroundFlusher struct {
mu sync.Mutex
stopCh chan struct{}
watcher *libkb.AppStateWatcher
// flushes counts flushes; tests use it.
flushes int
}

func (f *backgroundFlusher) start(m libkb.MetaContext, flush func(libkb.MetaContext)) {
f.mu.Lock()
defer f.mu.Unlock()
if f.stopCh != nil {
return
}
f.stopCh = make(chan struct{})
f.watcher = m.G().MobileAppState.NewWatcher()
stopCh, w := f.stopCh, f.watcher
go w.Run(m.G().MobileAppState.State(), stopCh, func(state keybase1.MobileAppState) bool {
if state == keybase1.MobileAppState_BACKGROUND {
flush(m)
f.mu.Lock()
f.flushes++
f.mu.Unlock()
}
return true
})
}

// stop ends the watcher and waits for it to exit.
func (f *backgroundFlusher) stop() {
f.mu.Lock()
stopCh, w := f.stopCh, f.watcher
f.stopCh, f.watcher = nil, nil
f.mu.Unlock()
if stopCh == nil {
return
}
close(stopCh)
w.Wait()
}
131 changes: 131 additions & 0 deletions go/avatars/appstate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package avatars

import (
"runtime"
"testing"
"time"

"github.com/keybase/client/go/libkb"
"github.com/keybase/client/go/protocol/keybase1"
"github.com/stretchr/testify/require"
)

// waitFlusher waits until f's watcher has acted on the current state and is
// waiting for the next change.
func waitFlusher(t *testing.T, f *backgroundFlusher) {
t.Helper()
require.Eventually(t, func() bool {
f.mu.Lock()
w := f.watcher
f.mu.Unlock()
if w == nil {
return false
}
_, caughtUp := w.CaughtUp()
return caughtUp
}, 10*time.Second, time.Millisecond, "watcher did not catch up")
}

func flushes(f *backgroundFlusher) int {
f.mu.Lock()
defer f.mu.Unlock()
return f.flushes
}

type bgSource interface {
libkb.AvatarLoaderSource
flusher() *backgroundFlusher
}

func (c *FullCachingSource) flusher() *backgroundFlusher { return &c.bgFlusher }
func (c *URLCachingSource) flusher() *backgroundFlusher { return &c.bgFlusher }

func forEachSource(t *testing.T, f func(t *testing.T, tc libkb.TestContext, s bgSource)) {
sources := map[string]func(t *testing.T, g *libkb.GlobalContext) bgSource{
"full": func(t *testing.T, g *libkb.GlobalContext) bgSource {
s := NewFullCachingSource(g, time.Hour, 10)
s.tempDir = t.TempDir()
return s
},
"url": func(_ *testing.T, _ *libkb.GlobalContext) bgSource {
return NewURLCachingSource(time.Hour, 10)
},
}
for name, mk := range sources {
t.Run(name, func(t *testing.T) {
tc := libkb.SetupTest(t, "avatars", 1)
defer tc.Cleanup()
f(t, tc, mk(t, tc.G))
})
}
}

func TestAvatarsFlushSeedsFromState(t *testing.T) {
forEachSource(t, func(t *testing.T, tc libkb.TestContext, s bgSource) {
m := libkb.NewMetaContextForTest(tc)
tc.G.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND)
s.StartBackgroundTasks(m)
defer s.StopBackgroundTasks(m)
waitFlusher(t, s.flusher())
require.Equal(t, 0, flushes(s.flusher()), "flushed without a transition into BACKGROUND")

for _, next := range []keybase1.MobileAppState{
keybase1.MobileAppState_FOREGROUND,
keybase1.MobileAppState_INACTIVE,
keybase1.MobileAppState_BACKGROUND,
} {
tc.G.MobileAppState.Update(next)
waitFlusher(t, s.flusher())
}
require.Equal(t, 1, flushes(s.flusher()))
})
}

func TestAvatarsMonitorExitsOnStop(t *testing.T) {
forEachSource(t, func(t *testing.T, tc libkb.TestContext, s bgSource) {
m := libkb.NewMetaContextForTest(tc)
// Warm up lazily started goroutines before taking the baseline.
s.StartBackgroundTasks(m)
s.StopBackgroundTasks(m)
baseline := runtime.NumGoroutine()

const cycles = 50
for range cycles {
s.StartBackgroundTasks(m)
waitFlusher(t, s.flusher())
s.StopBackgroundTasks(m)
}
require.Eventually(t, func() bool {
return runtime.NumGoroutine() < baseline+cycles/2
}, 10*time.Second, 10*time.Millisecond, "goroutines leaked across Start/Stop")
})
}

// Start/Stop racing app-state changes neither deadlocks nor leaks.
func TestAvatarsMonitorStress(t *testing.T) {
forEachSource(t, func(t *testing.T, tc libkb.TestContext, s bgSource) {
m := libkb.NewMetaContextForTest(tc)
baseline := runtime.NumGoroutine()
done := make(chan struct{})
go func() {
defer close(done)
states := []keybase1.MobileAppState{
keybase1.MobileAppState_FOREGROUND,
keybase1.MobileAppState_INACTIVE,
keybase1.MobileAppState_BACKGROUND,
keybase1.MobileAppState_BACKGROUNDACTIVE,
}
for i := range 400 {
tc.G.MobileAppState.Update(states[i%len(states)])
}
}()
for range 100 {
s.StartBackgroundTasks(m)
s.StopBackgroundTasks(m)
}
<-done
require.Eventually(t, func() bool {
return runtime.NumGoroutine() < baseline+10
}, 10*time.Second, 10*time.Millisecond, "goroutines leaked")
})
}
30 changes: 11 additions & 19 deletions go/avatars/fullcaching.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type FullCachingSource struct {
started bool
diskLRU *lru.DiskLRU
diskLRUCleanerCancel context.CancelFunc
bgFlusher backgroundFlusher
staleThreshold time.Duration
simpleSource libkb.AvatarLoaderSource

Expand Down Expand Up @@ -212,10 +213,15 @@ func (c *FullCachingSource) StartBackgroundTasks(mctx libkb.MetaContext) {
return
}
c.started = true
go c.monitorAppState(mctx)
c.bgFlusher.start(mctx, func(m libkb.MetaContext) {
c.debug(m, "monitorAppState: backgrounded")
if err := c.diskLRU.Flush(m.Ctx(), m.G()); err != nil {
c.debug(m, "monitorAppState: unable to flush diskLRU %v", err)
}
})
c.populateCacheCh = make(chan populateArg, 100)
for range 10 {
go c.populateCacheWorker(mctx)
go c.populateCacheWorker(mctx, c.populateCacheCh)
}
mctx, cancel := mctx.WithContextCancel()
c.diskLRUCleanerCancel = cancel
Expand All @@ -230,6 +236,7 @@ func (c *FullCachingSource) StopBackgroundTasks(mctx libkb.MetaContext) {
return
}
c.started = false
c.bgFlusher.stop()
close(c.populateCacheCh)
if c.diskLRUCleanerCancel != nil {
c.diskLRUCleanerCancel()
Expand All @@ -251,21 +258,6 @@ func (c *FullCachingSource) isStale(m libkb.MetaContext, item lru.DiskLRUEntry)
return m.G().GetClock().Now().Sub(item.Ctime) > c.staleThreshold
}

func (c *FullCachingSource) monitorAppState(m libkb.MetaContext) {
c.debug(m, "monitorAppState: starting up")
state := keybase1.MobileAppState_FOREGROUND
for {
<-m.G().MobileAppState.NextUpdate(state)
state = m.G().MobileAppState.State()
if state == keybase1.MobileAppState_BACKGROUND {
c.debug(m, "monitorAppState: backgrounded")
if err := c.diskLRU.Flush(m.Ctx(), m.G()); err != nil {
c.debug(m, "monitorAppState: unable to flush diskLRU %v", err)
}
}
}
}

func (c *FullCachingSource) processLRUHit(entry lru.DiskLRUEntry) (res lruEntry) {
var ok bool
if _, ok = entry.Value.(map[string]any); ok {
Expand Down Expand Up @@ -392,8 +384,8 @@ func (c *FullCachingSource) removeFile(m libkb.MetaContext, ent *lru.DiskLRUEntr
}
}

func (c *FullCachingSource) populateCacheWorker(m libkb.MetaContext) {
for arg := range c.populateCacheCh {
func (c *FullCachingSource) populateCacheWorker(m libkb.MetaContext, populateCacheCh <-chan populateArg) {
for arg := range populateCacheCh {
err := c.populateCacheJob(m, arg)
if err != nil {
c.debug(m, "populateCacheWorker: %s", err)
Expand Down
20 changes: 6 additions & 14 deletions go/avatars/urlcaching.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type URLCachingSource struct {
diskLRU *lru.DiskLRU
staleThreshold time.Duration
simpleSource *SimpleSource
bgFlusher backgroundFlusher

// testing only
staleFetchCh chan struct{}
Expand All @@ -30,10 +31,14 @@ func NewURLCachingSource(staleThreshold time.Duration, size int) *URLCachingSour
}

func (c *URLCachingSource) StartBackgroundTasks(m libkb.MetaContext) {
go c.monitorAppState(m)
c.bgFlusher.start(m, func(m libkb.MetaContext) {
c.debug(m, "monitorAppState: backgrounded")
c.diskLRU.Flush(m.Ctx(), m.G())
})
}

func (c *URLCachingSource) StopBackgroundTasks(m libkb.MetaContext) {
c.bgFlusher.stop()
c.diskLRU.Flush(m.Ctx(), m.G())
}

Expand All @@ -49,19 +54,6 @@ func (c *URLCachingSource) isStale(m libkb.MetaContext, item lru.DiskLRUEntry) b
return m.G().GetClock().Now().Sub(item.Ctime) > c.staleThreshold
}

func (c *URLCachingSource) monitorAppState(m libkb.MetaContext) {
c.debug(m, "monitorAppState: starting up")
state := keybase1.MobileAppState_FOREGROUND
for {
<-m.G().MobileAppState.NextUpdate(state)
state = m.G().MobileAppState.State()
if state == keybase1.MobileAppState_BACKGROUND {
c.debug(m, "monitorAppState: backgrounded")
c.diskLRU.Flush(m.Ctx(), m.G())
}
}
}

func (c *URLCachingSource) specLoad(m libkb.MetaContext, names []string, formats []keybase1.AvatarFormat) (res avatarLoadSpec, err error) {
for _, name := range names {
for _, format := range formats {
Expand Down
Loading