Skip to content

Commit 0e8a689

Browse files
Merge branch 'master' into nojima/HOTPOT-next-670-clean
# Conflicts: # rnmodules/react-native-kb/ios/Kb.mm # shared/constants/init/index.native.tsx # shared/ios/Keybase/AppDelegate.swift # shared/router-v2/hooks.native.tsx # shared/router-v2/router.native.tsx # shared/stores/daemon.tsx
2 parents c6f0ebe + b5a7bc2 commit 0e8a689

26 files changed

Lines changed: 868 additions & 203 deletions

File tree

go/bind/keybase.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ func ReadArr() (data []byte, err error) {
524524
// ensureConnection establishes the loopback connection if not already connected.
525525
// Must be called with connMutex held.
526526
func ensureConnection() error {
527+
start := time.Now()
527528
if !isInited() {
528529
return errors.New("keybase not initialized")
529530
}
@@ -536,7 +537,7 @@ func ensureConnection() error {
536537
if err != nil {
537538
return fmt.Errorf("Failed to dial loopback listener: %s", err)
538539
}
539-
log("Go: Established loopback connection")
540+
log("Go: Established loopback connection in %v", time.Since(start))
540541
return nil
541542
}
542543

@@ -590,6 +591,12 @@ func IsAppStateForeground() bool {
590591
return kbCtx.MobileAppState.State() == keybase1.MobileAppState_FOREGROUND
591592
}
592593

594+
// FlushLogs synchronously flushes any buffered log data to disk. Call this
595+
// before background suspension and after foreground resume to prevent log loss.
596+
func FlushLogs() {
597+
logger.FlushLogFile()
598+
}
599+
593600
func SetAppStateForeground() {
594601
if !isInited() {
595602
return
@@ -639,36 +646,39 @@ func waitForInit(maxDur time.Duration) error {
639646
}
640647
}
641648

642-
func BackgroundSync() {
649+
// BackgroundSync runs a short background sync pulse. Returns a non-empty status
650+
// string on early exit or error so Swift can log it via NSLog.
651+
func BackgroundSync() string {
643652
// On Android there is a race where this function can be called before Init when starting up in the
644653
// background. Let's wait a little bit here for Init to get run, and bail out if it never does.
645654
if err := waitForInit(5 * time.Second); err != nil {
646-
return
655+
return fmt.Sprintf("waitForInit timeout: %v", err)
647656
}
648657
defer kbCtx.Trace("BackgroundSync", nil)()
649658

650659
// Skip the sync if we aren't in the background
651660
if state := kbCtx.MobileAppState.State(); state != keybase1.MobileAppState_BACKGROUND {
652-
kbCtx.Log.Debug("BackgroundSync: skipping, app not in background state: %v", state)
653-
return
661+
msg := fmt.Sprintf("skipping, app not in background state: %v", state)
662+
kbCtx.Log.Debug("BackgroundSync: %s", msg)
663+
return msg
654664
}
655665

656666
nextState := keybase1.MobileAppState_BACKGROUNDACTIVE
657667
kbCtx.MobileAppState.Update(nextState)
658-
doneCh := make(chan struct{})
668+
resultCh := make(chan string, 1)
659669
go func() {
660-
defer func() { close(doneCh) }()
661670
select {
662671
case state := <-kbCtx.MobileAppState.NextUpdate(&nextState):
663672
// if literally anything happens, let's get out of here
664-
kbCtx.Log.Debug("BackgroundSync: bailing out early, appstate change: %v", state)
665-
return
673+
msg := fmt.Sprintf("bailing out early, appstate change: %v", state)
674+
kbCtx.Log.Debug("BackgroundSync: %s", msg)
675+
resultCh <- msg
666676
case <-time.After(10 * time.Second):
667677
kbCtx.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND)
668-
return
678+
resultCh <- "completed 10s window"
669679
}
670680
}()
671-
<-doneCh
681+
return <-resultCh
672682
}
673683

674684
// pushPendingMessageFailure sends at most one notification that a message

go/chat/localizer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,8 +845,9 @@ func (s *localizerPipeline) localizeConversation(ctx context.Context, uid gregor
845845
var maxValidID chat1.MessageID
846846
s.Debug(ctx, "localizing %d max msgs", len(maxMsgs))
847847
for _, mm := range maxMsgs {
848-
if mm.IsValid() &&
849-
utils.IsSnippetChatMessageType(mm.GetMessageType()) &&
848+
isValidSnippet := mm.IsValid() && utils.IsSnippetChatMessageType(mm.GetMessageType())
849+
isEphemeralErr := mm.IsError() && mm.Error().IsEphemeral
850+
if (isValidSnippet || isEphemeralErr) &&
850851
(conversationLocal.Info.SnippetMsg == nil ||
851852
conversationLocal.Info.SnippetMsg.GetMessageID() < mm.GetMessageID()) {
852853
conversationLocal.Info.SnippetMsg = new(chat1.MessageUnboxed)

go/chat/utils/utils.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,10 +1110,13 @@ func formatDuration(dur time.Duration) string {
11101110

11111111
func getMsgSnippetDecoration(msg chat1.MessageUnboxed) chat1.SnippetDecoration {
11121112
var msgBody chat1.MessageBody
1113-
if msg.IsValid() {
1113+
switch {
1114+
case msg.IsValid():
11141115
msgBody = msg.Valid().MessageBody
1115-
} else {
1116+
case msg.IsOutbox():
11161117
msgBody = msg.Outbox().Msg.MessageBody
1118+
default:
1119+
return chat1.SnippetDecoration_NONE
11171120
}
11181121
switch msg.GetMessageType() {
11191122
case chat1.MessageType_ATTACHMENT:
@@ -1214,14 +1217,20 @@ func GetMsgSnippetBody(ctx context.Context, g *globals.Context, uid gregor1.UID,
12141217
func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg chat1.MessageUnboxed,
12151218
conv chat1.ConversationLocal, currentUsername string,
12161219
) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) {
1217-
if !msg.IsValid() && !msg.IsOutbox() {
1218-
return chat1.SnippetDecoration_NONE, "", ""
1219-
}
12201220
defer func() {
12211221
if len(snippetDecorated) == 0 {
12221222
snippetDecorated = snippet
12231223
}
12241224
}()
1225+
if !msg.IsValid() && !msg.IsOutbox() {
1226+
if msg.IsError() && msg.Error().IsEphemeral {
1227+
if msg.Error().IsEphemeralExpired(time.Now()) {
1228+
return chat1.SnippetDecoration_EXPLODED_MESSAGE, "Message exploded.", ""
1229+
}
1230+
return chat1.SnippetDecoration_EXPLODING_MESSAGE, msg.Error().ErrMsg, ""
1231+
}
1232+
return chat1.SnippetDecoration_NONE, "", ""
1233+
}
12251234

12261235
var senderUsername string
12271236
if msg.IsValid() {

go/chat/utils/utils_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,36 @@ func TestSearchableRemoteConversationName(t *testing.T) {
10861086
searchableRemoteConversationNameFromStr("joshblum,zoommikem,mikem,zoomua,mikem", "mikem"))
10871087
}
10881088

1089+
func TestGetMsgSnippetEphemeralError(t *testing.T) {
1090+
ctx := context.Background()
1091+
conv := chat1.ConversationLocal{}
1092+
errMsg := "This exploding message is not available because this device was created after the message was sent"
1093+
1094+
// Non-expired ephemeral error: should surface the error message with EXPLODING_MESSAGE decoration.
1095+
msg := chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{
1096+
ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL,
1097+
ErrMsg: errMsg,
1098+
IsEphemeral: true,
1099+
Etime: gregor1.ToTime(time.Now().Add(time.Hour)),
1100+
MessageType: chat1.MessageType_TEXT,
1101+
})
1102+
decoration, snippet, _ := GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice")
1103+
require.Equal(t, chat1.SnippetDecoration_EXPLODING_MESSAGE, decoration)
1104+
require.Equal(t, errMsg, snippet)
1105+
1106+
// Expired ephemeral error: should show "Message exploded." with EXPLODED_MESSAGE decoration.
1107+
msg = chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{
1108+
ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL,
1109+
ErrMsg: errMsg,
1110+
IsEphemeral: true,
1111+
Etime: gregor1.ToTime(time.Now().Add(-time.Hour)),
1112+
MessageType: chat1.MessageType_TEXT,
1113+
})
1114+
decoration, snippet, _ = GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice")
1115+
require.Equal(t, chat1.SnippetDecoration_EXPLODED_MESSAGE, decoration)
1116+
require.Equal(t, "Message exploded.", snippet)
1117+
}
1118+
10891119
func TestStripUsernameFromConvName(t *testing.T) {
10901120
// Only the username as a complete segment is removed; "mikem" inside "zoommikem" must not be stripped
10911121
require.Equal(t, "joshblum,zoommikem,zoomua",

go/ephemeral/common_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ func TestEphemeralPluralization(t *testing.T) {
132132
require.Equal(t, humanMsg, pluralized)
133133

134134
pluralized = PluralizeErrorMessage(humanMsg, 2)
135-
require.Equal(t, "2 exploding messages are not available, because this device was created after it was sent", pluralized)
135+
require.Equal(t, "2 exploding messages are not available because this device was created after the messages were sent", pluralized)
136+
137+
pluralized = PluralizeErrorMessage(humanMsgWithPrefix(MemberAfterEKErrMsg), 3)
138+
require.Equal(t, "3 exploding messages are not available because you joined the team after the messages were sent", pluralized)
136139

137140
pluralized = PluralizeErrorMessage(DefaultHumanErrMsg, 2)
138141
require.Equal(t, "2 exploding messages are not available", pluralized)

go/ephemeral/errors.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ func newTransientEphemeralKeyError(err EphemeralKeyError) EphemeralKeyError {
8282
const (
8383
DefaultHumanErrMsg = "This exploding message is not available"
8484
DefaultPluralHumanErrMsg = "%d exploding messages are not available"
85-
DeviceCloneErrMsg = "cloned devices do not support exploding messages"
86-
DeviceCloneWithOneshotErrMsg = "to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance"
87-
DeviceAfterEKErrMsg = "because this device was created after it was sent"
88-
MemberAfterEKErrMsg = "because you joined the team after it was sent"
85+
DeviceCloneErrMsg = "because this device has been cloned"
86+
DeviceCloneWithOneshotErrMsg = "because this device is running in `oneshot` mode; to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance"
87+
DeviceAfterEKErrMsg = "because this device was created after the message was sent"
88+
MemberAfterEKErrMsg = "because you joined the team after the message was sent"
8989
DeviceStaleErrMsg = "because this device wasn't online to generate an exploding key"
9090
UserStaleErrMsg = "because you weren't online to generate new exploding keys"
9191
)
@@ -159,7 +159,7 @@ func humanMsgWithPrefix(humanMsg string) string {
159159
if humanMsg == "" {
160160
humanMsg = DefaultHumanErrMsg
161161
} else if !strings.Contains(humanMsg, DefaultHumanErrMsg) {
162-
humanMsg = fmt.Sprintf("%s, %s", DefaultHumanErrMsg, humanMsg)
162+
humanMsg = fmt.Sprintf("%s %s", DefaultHumanErrMsg, humanMsg)
163163
}
164164
return humanMsg
165165
}

go/kbfs/kbfsgit/runner.go

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,21 @@ func (r *runner) waitForJournal(ctx context.Context) error {
679679
r.printStageEndIfNeeded)
680680
}
681681

682+
// bestBranchFromCandidates selects the best branch for HEAD from a set of
683+
// candidate branch names (prefer main > master > alphabetically first).
684+
func bestBranchFromCandidates(best plumbing.ReferenceName, candidate plumbing.ReferenceName) plumbing.ReferenceName {
685+
switch {
686+
case candidate == "refs/heads/main":
687+
return candidate
688+
case candidate == "refs/heads/master" && best != "refs/heads/main":
689+
return candidate
690+
case best == "" || (best != "refs/heads/main" && best != "refs/heads/master" && candidate < best):
691+
return candidate
692+
default:
693+
return best
694+
}
695+
}
696+
682697
// handleList: From https://git-scm.com/docs/git-remote-helpers
683698
//
684699
// Lists the refs, one per line, in the format "<value> <name> [<attr>
@@ -705,9 +720,18 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
705720
if err != nil {
706721
return err
707722
}
723+
defer refs.Close()
708724

709-
var symRefs []string
725+
type symRefInfo struct {
726+
name plumbing.ReferenceName
727+
target plumbing.ReferenceName
728+
}
729+
var symRefs []symRefInfo
730+
hashRefNames := make(map[plumbing.ReferenceName]bool)
710731
hashesSeen := false
732+
// Track the best fallback branch for HEAD in case its target
733+
// doesn't exist (prefer main > master > alphabetically first).
734+
var bestBranch plumbing.ReferenceName
711735
for {
712736
ref, err := refs.Next()
713737
if errors.Cause(err) == io.EOF {
@@ -722,6 +746,11 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
722746
case plumbing.HashReference:
723747
value = ref.Hash().String()
724748
hashesSeen = true
749+
hashRefNames[ref.Name()] = true
750+
// Track best branch for fallback HEAD.
751+
if strings.HasPrefix(ref.Name().String(), "refs/heads/") {
752+
bestBranch = bestBranchFromCandidates(bestBranch, ref.Name())
753+
}
725754
case plumbing.SymbolicReference:
726755
value = "@" + ref.Target().String()
727756
default:
@@ -734,7 +763,10 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
734763
// cloning an empty repo will result in an error because
735764
// the HEAD symbolic ref points to a ref that doesn't
736765
// exist.
737-
symRefs = append(symRefs, refStr)
766+
symRefs = append(symRefs, symRefInfo{
767+
name: ref.Name(),
768+
target: ref.Target(),
769+
})
738770
continue
739771
}
740772
r.log.CDebugf(ctx, "Listing ref %s", refStr)
@@ -745,7 +777,30 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) {
745777
}
746778

747779
if hashesSeen && !forPush {
748-
for _, refStr := range symRefs {
780+
for _, sr := range symRefs {
781+
target := sr.target
782+
// If the symref target doesn't exist among hash refs,
783+
// rewrite it to point to the best available branch,
784+
// but only for HEAD. Other symrefs are emitted as-is.
785+
if !hashRefNames[target] {
786+
if sr.name == plumbing.HEAD {
787+
if bestBranch == "" {
788+
r.log.CDebugf(ctx,
789+
"Skipping HEAD symref %s -> %s (no branches available)",
790+
sr.name, target)
791+
continue
792+
}
793+
r.log.CDebugf(ctx,
794+
"Rewriting HEAD symref from %s to %s",
795+
target, bestBranch)
796+
target = bestBranch
797+
} else {
798+
r.log.CDebugf(ctx,
799+
"Emitting non-HEAD symref %s -> %s with unknown target",
800+
sr.name, target)
801+
}
802+
}
803+
refStr := "@" + target.String() + " " + sr.name.String() + "\n"
749804
r.log.CDebugf(ctx, "Listing symbolic ref %s", refStr)
750805
_, err = r.output.Write([]byte(refStr))
751806
if err != nil {
@@ -1827,6 +1882,53 @@ func (r *runner) handlePushBatch(ctx context.Context, args [][]string) (
18271882
return nil, err
18281883
}
18291884

1885+
// If HEAD points to a nonexistent ref, update it to point to the
1886+
// best available branch in the repo (prefer main > master > alphabetical).
1887+
// We intentionally repair HEAD even when the target was explicitly
1888+
// deleted in this batch, because a broken HEAD causes clone failures.
1889+
// This must happen before waitForJournal so the HEAD update is
1890+
// included in the same flush.
1891+
head, headErr := repo.Storer.Reference(plumbing.HEAD)
1892+
if headErr == nil && head.Type() == plumbing.SymbolicReference {
1893+
_, targetErr := repo.Storer.Reference(head.Target())
1894+
var bestBranch plumbing.ReferenceName
1895+
if targetErr == plumbing.ErrReferenceNotFound {
1896+
allRefs, refsErr := repo.References()
1897+
if refsErr == nil {
1898+
defer allRefs.Close()
1899+
for {
1900+
ref, nextErr := allRefs.Next()
1901+
if errors.Cause(nextErr) == io.EOF {
1902+
break
1903+
}
1904+
if nextErr != nil {
1905+
break
1906+
}
1907+
if ref.Type() != plumbing.HashReference {
1908+
continue
1909+
}
1910+
if !strings.HasPrefix(ref.Name().String(), "refs/heads/") {
1911+
continue
1912+
}
1913+
bestBranch = bestBranchFromCandidates(bestBranch, ref.Name())
1914+
}
1915+
}
1916+
}
1917+
if bestBranch != "" {
1918+
newHead := plumbing.NewSymbolicReference(
1919+
plumbing.HEAD, bestBranch)
1920+
if setErr := repo.Storer.SetReference(
1921+
newHead); setErr != nil {
1922+
r.log.CDebugf(ctx,
1923+
"Error updating HEAD to %s: %+v",
1924+
bestBranch, setErr)
1925+
} else {
1926+
r.log.CDebugf(ctx,
1927+
"Updated HEAD to point to %s", bestBranch)
1928+
}
1929+
}
1930+
}
1931+
18301932
err = r.waitForJournal(ctx)
18311933
if err != nil {
18321934
return nil, err

0 commit comments

Comments
 (0)