diff --git a/go/chat/globals/context.go b/go/chat/globals/context.go index 6bb8c4e55d44..f996d70b7c5f 100644 --- a/go/chat/globals/context.go +++ b/go/chat/globals/context.go @@ -252,11 +252,17 @@ func ChatCtx(ctx context.Context, g *Context, mode keybase1.TLFIdentifyBehavior, if _, ok := CtxTrace(res); !ok { res = CtxAddLogTags(res, g) } + if _, ok := ctxChatSession(res); !ok { + res = CtxStampChatSession(res, g) + } return res } func BackgroundChatCtx(sourceCtx context.Context, g *Context) context.Context { rctx := libkb.CopyTagsToBackground(sourceCtx) + if epoch, ok := ctxChatSession(sourceCtx); ok { + rctx = context.WithValue(rctx, chatSessionKey, epoch) + } in := CtxIdentifyNotifier(sourceCtx) if ident, breaks, ok := CtxIdentifyMode(sourceCtx); ok { diff --git a/go/chat/globals/globals.go b/go/chat/globals/globals.go index 911f042cd739..f334293423e8 100644 --- a/go/chat/globals/globals.go +++ b/go/chat/globals/globals.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "sync" "github.com/keybase/client/go/badges" "github.com/keybase/client/go/chat/types" @@ -52,6 +53,10 @@ type ChatContext struct { EmojiSource types.EmojiSource // emoji support EphemeralTracker types.EphemeralTracker // tracking of ephemeral msg caches ArchiveRegistry types.ChatArchiveRegistry // Metadata store of chat archives + + sessionMu sync.Mutex + sessionEpoch uint64 + sessionBlocked bool } func (c *ChatContext) Describe() string { diff --git a/go/chat/globals/session.go b/go/chat/globals/session.go new file mode 100644 index 000000000000..9ca769e5772d --- /dev/null +++ b/go/chat/globals/session.go @@ -0,0 +1,80 @@ +package globals + +import ( + "context" + + "github.com/keybase/client/go/libkb" +) + +type ( + chatSessionKeyTyp int +) + +var chatSessionKey chatSessionKeyTyp + +func chatSessionNotReadyErr() error { + return libkb.NewLoginRequiredError("chat session not ready") +} + +// Chat session gate: account switch is a logout barrier + login barrier. +// Zero value is ready so unit tests that never login still work. The service +// calls BeginChatLogout / MarkChatReady around stop/start of chat modules. +func (c *ChatContext) setChatSession(blocked bool) { + c.sessionMu.Lock() + defer c.sessionMu.Unlock() + c.sessionEpoch++ + c.sessionBlocked = blocked +} + +func (c *ChatContext) BeginChatLogout() { + c.setChatSession(true) +} + +func (c *ChatContext) MarkChatReady() { + c.setChatSession(false) +} + +func (c *ChatContext) chatSessionSnapshot() (epoch uint64, ready bool) { + c.sessionMu.Lock() + defer c.sessionMu.Unlock() + return c.sessionEpoch, !c.sessionBlocked +} + +func (c *ChatContext) ChatSessionReady() bool { + _, ready := c.chatSessionSnapshot() + return ready +} + +func (c *ChatContext) AssertChatSessionReady() error { + if c.ChatSessionReady() { + return nil + } + return chatSessionNotReadyErr() +} + +func CtxStampChatSession(ctx context.Context, g *Context) context.Context { + epoch, _ := g.chatSessionSnapshot() + return context.WithValue(ctx, chatSessionKey, epoch) +} + +func BindChatSession(ctx context.Context, g *Context) (context.Context, error) { + epoch, ready := g.chatSessionSnapshot() + if !ready { + return ctx, chatSessionNotReadyErr() + } + return context.WithValue(ctx, chatSessionKey, epoch), nil +} + +func ctxChatSession(ctx context.Context) (uint64, bool) { + epoch, ok := ctx.Value(chatSessionKey).(uint64) + return epoch, ok +} + +func ChatSessionStale(ctx context.Context, g *Context) bool { + stamp, ok := ctxChatSession(ctx) + if !ok { + return !g.ChatSessionReady() + } + epoch, ready := g.chatSessionSnapshot() + return !ready || stamp != epoch +} diff --git a/go/chat/globals/session_test.go b/go/chat/globals/session_test.go new file mode 100644 index 000000000000..19b444d633e5 --- /dev/null +++ b/go/chat/globals/session_test.go @@ -0,0 +1,35 @@ +package globals + +import ( + "context" + "testing" + + "github.com/keybase/client/go/libkb" + "github.com/stretchr/testify/require" +) + +func TestChatSessionGate(t *testing.T) { + c := &ChatContext{} + g := &Context{ChatContext: c} + require.True(t, c.ChatSessionReady()) + require.NoError(t, c.AssertChatSessionReady()) + + c.BeginChatLogout() + require.False(t, c.ChatSessionReady()) + err := c.AssertChatSessionReady() + require.Error(t, err) + _, ok := err.(libkb.LoginRequiredError) + require.True(t, ok) + _, err = BindChatSession(context.Background(), g) + require.Error(t, err) + + ctx := CtxStampChatSession(context.Background(), g) + require.True(t, ChatSessionStale(ctx, g)) + + c.MarkChatReady() + require.True(t, c.ChatSessionReady()) + ctx2, err := BindChatSession(context.Background(), g) + require.NoError(t, err) + require.False(t, ChatSessionStale(ctx2, g)) + require.True(t, ChatSessionStale(ctx, g), "old epoch must be stale after login") +} diff --git a/go/chat/localizer.go b/go/chat/localizer.go index 880c705632fb..2b8eff9f8898 100644 --- a/go/chat/localizer.go +++ b/go/chat/localizer.go @@ -548,6 +548,9 @@ func (s *localizerPipeline) jobPulled(ctx context.Context, job *localizerPipelin func (s *localizerPipeline) localizeConversations(localizeJob *localizerPipelineJob) (err error) { ctx := localizeJob.ctx uid := localizeJob.uid + if globals.ChatSessionStale(ctx, s.G()) { + return storage.NewAbortedError() + } defer s.Trace(ctx, &err, "localizeConversations")() // Fetch conversation local information in parallel diff --git a/go/chat/retry.go b/go/chat/retry.go index eb640b1cf54d..c7665df4878a 100644 --- a/go/chat/retry.go +++ b/go/chat/retry.go @@ -299,7 +299,7 @@ func (f *FetchRetrier) Failure(ctx context.Context, uid gregor1.UID, desc types. defer f.Trace(ctx, nil, "Failure(%s)", desc)() f.Lock() defer f.Unlock() - if !f.running { + if !f.running || !f.G().ChatSessionReady() { f.Debug(ctx, "Failure: not starting new retrier, not running") return } diff --git a/go/chat/session_rpc_test.go b/go/chat/session_rpc_test.go new file mode 100644 index 000000000000..9bae226a432d --- /dev/null +++ b/go/chat/session_rpc_test.go @@ -0,0 +1,35 @@ +package chat + +import ( + "context" + "testing" + + "github.com/keybase/client/go/chat/storage" + "github.com/keybase/client/go/protocol/chat1" + "github.com/keybase/client/go/protocol/gregor1" + "github.com/stretchr/testify/require" +) + +func TestInboxLoaderRejectsUntilReady(t *testing.T) { + ctc := makeChatTestContext(t, "TestInboxLoaderRejectsUntilReady", 1) + defer ctc.cleanup() + users := ctc.users() + h := ctc.as(t, users[0]).h + tc := ctc.world.Tcs[users[0].Username] + uid := gregor1.UID(users[0].GetUID().ToBytes()) + loader := NewUIInboxLoader(tc.Context()) + tc.ChatG.UIInboxLoader = loader + loader.Start(context.Background(), uid) + defer func() { <-loader.Stop(context.Background()) }() + + // Unstamped ctx: stale iff ChatSessionReady is false, independent of setup stamps. + ctx := context.Background() + h.G().BeginChatLogout() + err := loader.UpdateConvs(ctx, []chat1.ConversationID{{0x01}}) + require.Error(t, err) + require.ErrorAs(t, err, new(storage.AbortedError)) + + h.G().MarkChatReady() + _, err = loader.sessionUID(ctx) + require.NoError(t, err) +} diff --git a/go/chat/storage/inbox.go b/go/chat/storage/inbox.go index 9ad9407c2bf5..eca69be7dc48 100644 --- a/go/chat/storage/inbox.go +++ b/go/chat/storage/inbox.go @@ -202,6 +202,9 @@ func (i *Inbox) readDiskVersions(ctx context.Context, uid gregor1.UID, useInMemo if err := isAbortedRequest(ctx); err != nil { return ibox, err } + if globals.ChatSessionStale(ctx, i.G()) { + return ibox, NewAbortedError() + } if err := i.missIfWrongSessionUID(uid); err != nil { return ibox, err } @@ -272,6 +275,9 @@ func (i *Inbox) readDiskIndex(ctx context.Context, uid gregor1.UID, useInMemory if err := isAbortedRequest(ctx); err != nil { return ibox, err } + if globals.ChatSessionStale(ctx, i.G()) { + return ibox, NewAbortedError() + } if err := i.missIfWrongSessionUID(uid); err != nil { return ibox, err } diff --git a/go/chat/uiinboxloader.go b/go/chat/uiinboxloader.go index b0cb30c5ae81..c10081cf1148 100644 --- a/go/chat/uiinboxloader.go +++ b/go/chat/uiinboxloader.go @@ -13,6 +13,7 @@ import ( "time" "github.com/keybase/client/go/chat/globals" + "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/libkb" @@ -122,6 +123,16 @@ func (h *UIInboxLoader) doStopLocked(ctx context.Context) chan struct{} { return ch } +func (h *UIInboxLoader) sessionUID(ctx context.Context) (gregor1.UID, error) { + h.Lock() + started, uid := h.started, h.uid + h.Unlock() + if !started || uid.IsNil() || globals.ChatSessionStale(ctx, h.G()) { + return uid, storage.NewAbortedError() + } + return uid, nil +} + func (h *UIInboxLoader) getChatUI(ctx context.Context) (libkb.ChatUI, error) { if h.G().UIRouter == nil { return nil, errors.New("no UI router available") @@ -137,7 +148,7 @@ func (h *UIInboxLoader) getChatUI(ctx context.Context) (libkb.ChatUI, error) { return ui, nil } -func (h *UIInboxLoader) presentUnverifiedInbox(ctx context.Context, convs []types.RemoteConversation, +func (h *UIInboxLoader) presentUnverifiedInbox(ctx context.Context, uid gregor1.UID, convs []types.RemoteConversation, offline bool, ) (res chat1.UnverifiedInboxUIItems, err error) { for _, rawConv := range convs { @@ -146,7 +157,7 @@ func (h *UIInboxLoader) presentUnverifiedInbox(ctx context.Context, convs []type rawConv.Conv.GetConvID()) continue } - res.Items = append(res.Items, utils.PresentRemoteConversation(ctx, h.G(), h.uid, rawConv)) + res.Items = append(res.Items, utils.PresentRemoteConversation(ctx, h.G(), uid, rawConv)) } res.Offline = offline return res, err @@ -172,28 +183,35 @@ func (h *UIInboxLoader) flushConvBatch() (err error) { } ctx := globals.ChatCtx(context.Background(), h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer h.Trace(ctx, &err, "flushConvBatch")() + uid, sessErr := h.sessionUID(ctx) var convs []chat1.ConversationLocal for _, conv := range h.convTransmitBatch { convs = append(convs, conv) } h.lastBatchFlush = h.clock.Now() h.convTransmitBatch = make(map[chat1.ConvIDStr]chat1.ConversationLocal) // clear batch always + if sessErr != nil { + return sessErr + } h.Debug(ctx, "flushConvBatch: transmitting %d convs", len(convs)) defer func() { + if _, sessErr := h.sessionUID(ctx); sessErr != nil { + return + } if err != nil { h.Debug(ctx, "flushConvBatch: failed to transmit, retrying convs: num: %d err: %s", len(convs), err) for _, conv := range convs { - h.G().FetchRetrier.Failure(ctx, h.uid, + h.G().FetchRetrier.Failure(ctx, uid, NewConversationRetry(h.G(), conv.GetConvID(), &conv.Info.Triple.Tlfid, InboxLoad)) } } - if err = h.G().InboxSource.MergeLocalMetadata(ctx, h.uid, convs); err != nil { + if err = h.G().InboxSource.MergeLocalMetadata(ctx, uid, convs); err != nil { h.Debug(ctx, "flushConvBatch: unable to write inbox local metadata: %s", err) } }() start := time.Now() - dat, err := json.Marshal(utils.PresentConversationLocals(ctx, h.G(), h.uid, convs, + dat, err := json.Marshal(utils.PresentConversationLocals(ctx, h.G(), uid, convs, utils.PresentParticipantsModeInclude)) if err != nil { return err @@ -213,14 +231,20 @@ func (h *UIInboxLoader) flushConvBatch() (err error) { func (h *UIInboxLoader) flushUnverified(r unverifiedResponse) (err error) { ctx := context.Background() + uid, sessErr := h.sessionUID(ctx) + if sessErr != nil { + return sessErr + } defer func() { if err != nil { h.Debug(ctx, "flushUnverified: failed to transmit, retrying: %s", err) - h.G().FetchRetrier.Failure(ctx, h.uid, NewFullInboxRetry(h.G(), r.Query)) + if _, sessErr := h.sessionUID(ctx); sessErr == nil { + h.G().FetchRetrier.Failure(ctx, uid, NewFullInboxRetry(h.G(), r.Query)) + } } }() start := time.Now() - uires, err := h.presentUnverifiedInbox(ctx, r.Convs, h.G().InboxSource.IsOffline(ctx)) + uires, err := h.presentUnverifiedInbox(ctx, uid, r.Convs, h.G().InboxSource.IsOffline(ctx)) if err != nil { h.Debug(ctx, "flushUnverified: failed to present untrusted inbox, failing: %s", err.Error()) return err @@ -250,20 +274,26 @@ func (h *UIInboxLoader) flushUnverified(r unverifiedResponse) (err error) { func (h *UIInboxLoader) flushFailed(r failedResponse) { ctx := context.Background() + uid, sessErr := h.sessionUID(ctx) + if sessErr != nil { + return + } ui, err := h.getChatUI(ctx) h.Debug(ctx, "flushFailed: transmitting: %s", r.Conv.GetConvID()) if err == nil { if err := ui.ChatInboxFailed(ctx, chat1.ChatInboxFailedArg{ ConvID: r.Conv.GetConvID(), - Error: utils.PresentConversationErrorLocal(ctx, h.G(), h.uid, *r.Conv.Error), + Error: utils.PresentConversationErrorLocal(ctx, h.G(), uid, *r.Conv.Error), }); err != nil { h.Debug(ctx, "flushFailed: failed to send failed conv: %s", err) } } // If we get a transient failure, add this to the retrier queue if r.Conv.Error.Typ == chat1.ConversationErrorType_TRANSIENT { - h.G().FetchRetrier.Failure(ctx, h.uid, - NewConversationRetry(h.G(), r.Conv.GetConvID(), &r.Conv.Info.Triple.Tlfid, InboxLoad)) + if _, sessErr := h.sessionUID(ctx); sessErr == nil { + h.G().FetchRetrier.Failure(ctx, uid, + NewConversationRetry(h.G(), r.Conv.GetConvID(), &r.Conv.Info.Triple.Tlfid, InboxLoad)) + } } } @@ -301,7 +331,11 @@ func (h *UIInboxLoader) LoadNonblock(ctx context.Context, query *chat1.GetInboxL maxUnbox *int, skipUnverified bool, ) (err error) { defer h.Trace(ctx, &err, "LoadNonblock")() - uid := h.uid + uid, err := h.sessionUID(ctx) + if err != nil { + h.Debug(ctx, "LoadNonblock: rejecting, loader not started for current session") + return err + } // Retry helpers retryInboxLoad := func() { h.G().FetchRetrier.Failure(ctx, uid, NewFullInboxRetry(h.G(), query)) @@ -313,6 +347,12 @@ func (h *UIInboxLoader) LoadNonblock(ctx context.Context, query *chat1.GetInboxL // handle errors on the main processing thread, any errors during localizaton are handled // in the goroutine for localization callbacks if err != nil { + if _, ok := err.(storage.AbortedError); ok { + return + } + if _, sessErr := h.sessionUID(ctx); sessErr != nil { + return + } if query != nil && len(query.ConvIDs) > 0 { h.Debug(ctx, "LoadNonblock: failed to load convID query, retrying all convs") for _, convID := range query.ConvIDs { @@ -678,22 +718,32 @@ func (h *UIInboxLoader) OnLogout(mctx libkb.MetaContext) error { func (h *UIInboxLoader) getInboxFromQuery(ctx context.Context) (inbox types.Inbox, err error) { defer h.Trace(ctx, &err, "getInboxFromQuery")() + uid, err := h.sessionUID(ctx) + if err != nil { + return inbox, err + } query := h.Query() rquery, _, err := h.G().InboxSource.GetInboxQueryLocalToRemote(ctx, &query) if err != nil { return inbox, err } - return h.G().InboxSource.ReadUnverified(ctx, h.uid, types.InboxSourceDataSourceAll, rquery) + return h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, rquery) } func (h *UIInboxLoader) flushLayout(reselectMode chat1.InboxLayoutReselectMode) (err error) { ctx := globals.ChatCtx(context.Background(), h.G(), keybase1.TLFIdentifyBehavior_GUI, nil, nil) defer h.Trace(ctx, &err, "flushLayout")() + uid, err := h.sessionUID(ctx) + if err != nil { + return err + } defer func() { if err != nil { h.Debug(ctx, "flushLayout: failed to transmit, retrying: %s", err) - q := h.Query() - h.G().FetchRetrier.Failure(ctx, h.uid, NewFullInboxRetry(h.G(), &q)) + if _, sessErr := h.sessionUID(ctx); sessErr == nil { + q := h.Query() + h.G().FetchRetrier.Failure(ctx, uid, NewFullInboxRetry(h.G(), &q)) + } } }() ui, err := h.getChatUI(ctx) @@ -808,6 +858,10 @@ func (h *UIInboxLoader) UpdateLayout(ctx context.Context, reselectMode chat1.Inb reason string, ) { defer h.Trace(ctx, nil, "UpdateLayout: %s", reason)() + if _, err := h.sessionUID(ctx); err != nil { + h.Debug(ctx, "UpdateLayout: rejecting, loader not started for current session") + return + } select { case h.layoutCh <- reselectMode: default: @@ -841,6 +895,9 @@ func (h *UIInboxLoader) UpdateLayoutFromSubteamRename(ctx context.Context, convs func (h *UIInboxLoader) UpdateConvs(ctx context.Context, convIDs []chat1.ConversationID) (err error) { defer h.Trace(ctx, &err, "UpdateConvs")() + if _, err := h.sessionUID(ctx); err != nil { + return err + } query := chat1.GetInboxLocalQuery{ ComputeActiveList: true, ConvIDs: convIDs, diff --git a/go/chat/uiinboxloader_test.go b/go/chat/uiinboxloader_test.go index b7ecd6384d93..6ed9908f087c 100644 --- a/go/chat/uiinboxloader_test.go +++ b/go/chat/uiinboxloader_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/keybase/client/go/chat/globals" + "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/kbtest" "github.com/keybase/client/go/libkb" @@ -531,3 +532,25 @@ func TestPrepareShareConversations(t *testing.T) { require.Equal(t, "id2", calls[1][0].ConvID) }) } + +func TestUIInboxLoaderStopRejectsUnbox(t *testing.T) { + ctc := makeChatTestContext(t, "TestUIInboxLoaderStopRejectsUnbox", 1) + defer ctc.cleanup() + users := ctc.users() + ctx := ctc.as(t, users[0]).startCtx + tc := ctc.world.Tcs[users[0].Username] + uidA := gregor1.UID(users[0].GetUID().ToBytes()) + uidB := gregor1.UID([]byte("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + loader := NewUIInboxLoader(tc.Context()) + tc.ChatG.UIInboxLoader = loader + loader.Start(ctx, uidA) + <-loader.Stop(ctx) + err := loader.UpdateConvs(ctx, []chat1.ConversationID{{0x01}}) + require.Error(t, err) + require.ErrorAs(t, err, new(storage.AbortedError)) + + loader.Start(ctx, uidB) + defer func() { <-loader.Stop(ctx) }() + require.True(t, loader.uid.Eq(uidB)) + require.False(t, loader.uid.Eq(uidA)) +} diff --git a/go/service/canceling.go b/go/service/canceling.go index e477fa9aa78d..314b25becdbd 100644 --- a/go/service/canceling.go +++ b/go/service/canceling.go @@ -3,10 +3,31 @@ package service import ( "context" + "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/libkb" "github.com/keybase/go-framed-msgpack-rpc/rpc" ) +func ChatSessionGatingProtocol(g *globals.Context, prot rpc.Protocol) (res rpc.Protocol) { + res.Name = prot.Name + res.WrapError = prot.WrapError + res.Methods = make(map[string]rpc.ServeHandlerDescription) + for name, ldesc := range prot.Methods { + var newDesc rpc.ServeHandlerDescription + desc := ldesc + newDesc.MakeArg = desc.MakeArg + newDesc.Handler = func(ctx context.Context, arg any) (any, error) { + ctx, err := globals.BindChatSession(ctx, g) + if err != nil { + return nil, err + } + return desc.Handler(ctx, arg) + } + res.Methods[name] = newDesc + } + return res +} + func CancelingProtocol(g *libkb.GlobalContext, prot rpc.Protocol, reason libkb.RPCCancelerReason) (res rpc.Protocol) { res.Name = prot.Name res.WrapError = prot.WrapError diff --git a/go/service/chat_session_protocol_test.go b/go/service/chat_session_protocol_test.go new file mode 100644 index 000000000000..3f3708b4d661 --- /dev/null +++ b/go/service/chat_session_protocol_test.go @@ -0,0 +1,41 @@ +package service + +import ( + "context" + "testing" + + "github.com/keybase/client/go/chat/globals" + "github.com/keybase/client/go/libkb" + "github.com/keybase/go-framed-msgpack-rpc/rpc" + "github.com/stretchr/testify/require" +) + +func TestChatSessionGatingProtocol(t *testing.T) { + g := globals.NewContext(&libkb.GlobalContext{}, &globals.ChatContext{}) + called := false + prot := ChatSessionGatingProtocol(g, rpc.Protocol{ + Name: "test", + Methods: map[string]rpc.ServeHandlerDescription{ + "echo": { + MakeArg: func() any { return new(int) }, + Handler: func(ctx context.Context, _ any) (any, error) { + called = true + require.False(t, globals.ChatSessionStale(ctx, g)) + return 1, nil + }, + }, + }, + }) + + g.BeginChatLogout() + _, err := prot.Methods["echo"].Handler(context.Background(), nil) + require.Error(t, err) + _, ok := err.(libkb.LoginRequiredError) + require.True(t, ok) + require.False(t, called) + + g.MarkChatReady() + _, err = prot.Methods["echo"].Handler(context.Background(), nil) + require.NoError(t, err) + require.True(t, called) +} diff --git a/go/service/main.go b/go/service/main.go index e780fb65be58..34d4af454fa8 100644 --- a/go/service/main.go +++ b/go/service/main.go @@ -175,8 +175,8 @@ func (d *Service) RegisterProtocols(srv *rpc.Server, xp rpc.Transporter, connID keybase1.RekeyProtocol(NewRekeyHandler2(xp, g, d.rekeyMaster)), keybase1.NotifyFSRequestProtocol(newNotifyFSRequestHandler(xp, g)), keybase1.GregorProtocol(newGregorRPCHandler(xp, g, d.gregor)), - CancelingProtocol(g, chat1.LocalProtocol(newChatLocalHandler(xp, cg, d.gregor)), - libkb.RPCCancelerReasonAll), + ChatSessionGatingProtocol(cg, CancelingProtocol(g, chat1.LocalProtocol(newChatLocalHandler(xp, cg, d.gregor)), + libkb.RPCCancelerReasonAll)), keybase1.SimpleFSProtocol(NewSimpleFSHandler(xp, g)), keybase1.LogsendProtocol(NewLogsendHandler(xp, g)), CancelingProtocol(g, keybase1.TeamsProtocol(NewTeamsHandler(xp, connID, cg, d)), @@ -484,6 +484,7 @@ func (d *Service) startChatModules() { g.LiveLocationTracker.Start(context.Background(), uid) g.BotCommandManager.Start(context.Background(), uid) g.UIInboxLoader.Start(context.Background(), uid) + g.MarkChatReady() g.PushShutdownHook(d.stopChatModules) } d.purgeOldChatAttachmentData() @@ -1022,6 +1023,9 @@ func (d *Service) OnLogout(m libkb.MetaContext) (err error) { m.Debug("Service#OnLogout: %s", s) } + log("gating chat session") + d.ChatG().BeginChatLogout() + log("canceling live RPCs") d.G().RPCCanceler.CancelLiveContexts(libkb.RPCCancelerReasonLogout)