From e6ac912d418ccde5419498d2379665621fa3b467 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 3 Mar 2026 10:36:41 -0500 Subject: [PATCH 01/42] typo (#28965) --- shared/desktop/CHANGELOG.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/desktop/CHANGELOG.txt b/shared/desktop/CHANGELOG.txt index 170335b00409..94f072196ab0 100644 --- a/shared/desktop/CHANGELOG.txt +++ b/shared/desktop/CHANGELOG.txt @@ -1,4 +1,4 @@ -• On iOS quicky share to recent conversations +• On iOS quickly share to recent conversations • Emoji 16 support • iOS HEIC Avatar support • Better sharing/push support From bc321cb873783443d745da979f0731c46875078c Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 10 Mar 2026 15:00:24 -0400 Subject: [PATCH 02/42] add tabIndex to stop going from hidden to showing through widget causing the search to be focused (#29003) * add tabIndex to stop going from hidden to showing through widget causing the search to be focused --- shared/common-adapters/plain-input.d.ts | 1 + shared/common-adapters/plain-input.desktop.tsx | 3 ++- shared/common-adapters/search-filter.tsx | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shared/common-adapters/plain-input.d.ts b/shared/common-adapters/plain-input.d.ts index eb59d90d6bb1..45d5ce06f9f3 100644 --- a/shared/common-adapters/plain-input.d.ts +++ b/shared/common-adapters/plain-input.d.ts @@ -98,6 +98,7 @@ export type Props = { onKeyDown?: (event: React.KeyboardEvent) => void onKeyUp?: (event: React.KeyboardEvent) => void spellCheck?: boolean + tabIndex?: number // desktop only // Mobile only children?: React.ReactNode allowFontScaling?: boolean diff --git a/shared/common-adapters/plain-input.desktop.tsx b/shared/common-adapters/plain-input.desktop.tsx index acbffba61cbc..2ee05c107220 100644 --- a/shared/common-adapters/plain-input.desktop.tsx +++ b/shared/common-adapters/plain-input.desktop.tsx @@ -40,7 +40,7 @@ const PlainInput = React.memo( const {growAndScroll, multiline, onFocus: _onFocus, selectTextOnFocus, onChangeText} = p const {maxBytes, globalCaptureKeypress, onBlur, onClick, style, resize, maxLength} = p const {rowsMin, rowsMax, textType, padding, flexable = true, type} = p - const {autoFocus, allowKeyboardEvents, placeholder, spellCheck, disabled, value, className} = p + const {autoFocus, allowKeyboardEvents, placeholder, spellCheck, disabled, value, className, tabIndex} = p const inputRef = React.useRef(null) const isComposingIMERef = React.useRef(false) const mountedRef = React.useRef(true) @@ -216,6 +216,7 @@ const PlainInput = React.memo( placeholder, ref: inputRef, spellCheck, + tabIndex, value, ...(maxLength ? {maxLength} : {}), ...(disabled ? {readOnly: true} : {}), diff --git a/shared/common-adapters/search-filter.tsx b/shared/common-adapters/search-filter.tsx index 0590aedf9182..44ba9eeebb52 100644 --- a/shared/common-adapters/search-filter.tsx +++ b/shared/common-adapters/search-filter.tsx @@ -57,6 +57,7 @@ type Props = { onKeyDown?: (event: React.KeyboardEvent) => void onKeyUp?: (event: React.KeyboardEvent) => void onKeyPress?: (event: NativeSyntheticEvent<{key: string}>) => void + tabIndex?: number // desktop only measureRef?: React.RefObject } @@ -201,6 +202,7 @@ const SearchFilter = React.forwardRef(function SearchFil onKeyUp={props.onKeyUp} onKeyPress={props.onKeyPress} onEnterKeyDown={props.onEnterKeyDown} + tabIndex={props.tabIndex} ref={inputRef} hideBorder={true} containerStyle={styles.inputContainer} From 0e7b9db5e694a228557eefd77e5620455f75642e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 10 Mar 2026 16:03:31 -0400 Subject: [PATCH 03/42] fix widget more (#29005) * use dummy input for desktop chat search * fix styling, use dummy for desktop also * lint --- shared/chat/inbox/filter-row.tsx | 50 +++++++++++++++---- shared/common-adapters/plain-input.d.ts | 1 - .../common-adapters/plain-input.desktop.tsx | 3 +- shared/common-adapters/search-filter.tsx | 2 - 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/shared/chat/inbox/filter-row.tsx b/shared/chat/inbox/filter-row.tsx index 8a5651be6445..edb820dd9ca0 100644 --- a/shared/chat/inbox/filter-row.tsx +++ b/shared/chat/inbox/filter-row.tsx @@ -75,6 +75,7 @@ const ConversationFilterInput = React.memo(function ConversationFilterInput(ownP appendNewChatBuilder() }, [appendNewChatBuilder]) Kb.useHotKey('mod+n', onHotKeys) + Kb.useHotKey('mod+k', onStartSearch) React.useEffect(() => { if (isSearching) { @@ -82,28 +83,31 @@ const ConversationFilterInput = React.memo(function ConversationFilterInput(ownP } }, [isSearching]) - const searchInput = ( + const searchInput = isSearching ? ( + ) : ( + + + + + {Kb.Styles.isMobile ? 'Search' : `Search (\u2318K)`} + + + ) return ( void onKeyUp?: (event: React.KeyboardEvent) => void spellCheck?: boolean - tabIndex?: number // desktop only // Mobile only children?: React.ReactNode allowFontScaling?: boolean diff --git a/shared/common-adapters/plain-input.desktop.tsx b/shared/common-adapters/plain-input.desktop.tsx index 2ee05c107220..acbffba61cbc 100644 --- a/shared/common-adapters/plain-input.desktop.tsx +++ b/shared/common-adapters/plain-input.desktop.tsx @@ -40,7 +40,7 @@ const PlainInput = React.memo( const {growAndScroll, multiline, onFocus: _onFocus, selectTextOnFocus, onChangeText} = p const {maxBytes, globalCaptureKeypress, onBlur, onClick, style, resize, maxLength} = p const {rowsMin, rowsMax, textType, padding, flexable = true, type} = p - const {autoFocus, allowKeyboardEvents, placeholder, spellCheck, disabled, value, className, tabIndex} = p + const {autoFocus, allowKeyboardEvents, placeholder, spellCheck, disabled, value, className} = p const inputRef = React.useRef(null) const isComposingIMERef = React.useRef(false) const mountedRef = React.useRef(true) @@ -216,7 +216,6 @@ const PlainInput = React.memo( placeholder, ref: inputRef, spellCheck, - tabIndex, value, ...(maxLength ? {maxLength} : {}), ...(disabled ? {readOnly: true} : {}), diff --git a/shared/common-adapters/search-filter.tsx b/shared/common-adapters/search-filter.tsx index 44ba9eeebb52..0590aedf9182 100644 --- a/shared/common-adapters/search-filter.tsx +++ b/shared/common-adapters/search-filter.tsx @@ -57,7 +57,6 @@ type Props = { onKeyDown?: (event: React.KeyboardEvent) => void onKeyUp?: (event: React.KeyboardEvent) => void onKeyPress?: (event: NativeSyntheticEvent<{key: string}>) => void - tabIndex?: number // desktop only measureRef?: React.RefObject } @@ -202,7 +201,6 @@ const SearchFilter = React.forwardRef(function SearchFil onKeyUp={props.onKeyUp} onKeyPress={props.onKeyPress} onEnterKeyDown={props.onEnterKeyDown} - tabIndex={props.tabIndex} ref={inputRef} hideBorder={true} containerStyle={styles.inputContainer} From efe2d20a51b3ce75eb7f7979cd27ff6c679351ea Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 10 Mar 2026 17:16:57 -0400 Subject: [PATCH 04/42] Fix outstanding appendGUILogs session in node process (#29001) (#29006) The node engine's NativeTransport.packetize_data forwards all responses to the renderer without processing them locally, so RPCs sent from the node process never get responses. The logger's periodic dump was sending appendGUILogs through the node engine, creating a session that stayed outstanding forever. Check process.type to skip log sending in the node (main) process. --- shared/logger/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shared/logger/index.tsx b/shared/logger/index.tsx index 074a79483655..2ec57274e58e 100644 --- a/shared/logger/index.tsx +++ b/shared/logger/index.tsx @@ -109,8 +109,13 @@ class AggregateLoggerImpl { sendLogsToService = async (lines: Array) => { if (!isMobile) { - // don't want main node thread making these calls + // don't want main node thread making these calls — the node engine's + // NativeTransport forwards responses to the renderer without processing + // them locally, so RPCs sent from node never get responses. try { + if (typeof process !== 'undefined' && process.type !== 'renderer') { + return await Promise.resolve() + } const {hasEngine} = require('../engine/require') as {hasEngine: typeof HasEngineType} if (!hasEngine()) { return await Promise.resolve() From cc71edfcbe590b8e76f91531c57d9c5ca7a5855c Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 10 Mar 2026 17:23:22 -0400 Subject: [PATCH 05/42] Bump version to 6.6.1 (#29007) --- go/libkb/version.go | 2 +- shared/android/app/build.gradle | 2 +- shared/ios/Keybase/Info.plist | 2 +- shared/ios/KeybaseShare/Info.plist | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go/libkb/version.go b/go/libkb/version.go index d8b23ce1ad0e..9d57a10e1134 100644 --- a/go/libkb/version.go +++ b/go/libkb/version.go @@ -4,4 +4,4 @@ package libkb // Version is the current version (should be MAJOR.MINOR.PATCH) -const Version = "6.6.0" +const Version = "6.6.1" diff --git a/shared/android/app/build.gradle b/shared/android/app/build.gradle index 43cfc1c7c8ad..cdc3d05e206d 100644 --- a/shared/android/app/build.gradle +++ b/shared/android/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: "com.facebook.react" apply plugin: 'com.github.triplet.play' // KB: app version -def VERSION_NAME = "6.6.0" +def VERSION_NAME = "6.6.1" // KB: Number of commits, like ios Integer getVersionCode() { diff --git a/shared/ios/Keybase/Info.plist b/shared/ios/Keybase/Info.plist index fb65c31b600d..9d97bbe2ad2a 100644 --- a/shared/ios/Keybase/Info.plist +++ b/shared/ios/Keybase/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 6.6.0 + 6.6.1 CFBundleSignature ???? CFBundleURLTypes diff --git a/shared/ios/KeybaseShare/Info.plist b/shared/ios/KeybaseShare/Info.plist index d7a771229ac6..52eba07f328d 100644 --- a/shared/ios/KeybaseShare/Info.plist +++ b/shared/ios/KeybaseShare/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 6.6.0 + 6.6.1 CFBundleVersion 200 NSExtension From 0c522b47b602a8e7cbb7d0abc065e81e680b7dfa Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 11 Mar 2026 18:18:57 -0400 Subject: [PATCH 06/42] Fix missing copy/move menu item in files tab (#29009) The layout correctly computed moveOrCopy: true for files inside TLFs, but no menu item was ever created from that flag. Add the missing itemMoveOrCopy entry that calls setMoveOrCopySource and showMoveOrCopy to open the destination picker. --- .../path-item-action/menu-container.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/shared/fs/common/path-item-action/menu-container.tsx b/shared/fs/common/path-item-action/menu-container.tsx index 996f366ea73c..b7ceff503364 100644 --- a/shared/fs/common/path-item-action/menu-container.tsx +++ b/shared/fs/common/path-item-action/menu-container.tsx @@ -31,7 +31,7 @@ const Container = (op: OwnProps) => { const pathItemActionMenu = s.pathItemActionMenu const fileContext = s.fileContext.get(path) || FS.emptyFileContext const {cancelDownload, setPathItemActionMenuView, download, newFolderRow} = s.dispatch - const {favoriteIgnore, startRename, dismissDownload} = s.dispatch + const {favoriteIgnore, startRename, dismissDownload, setMoveOrCopySource, showMoveOrCopy} = s.dispatch const {openPathInSystemFileManagerDesktop} = s.dispatch.dynamic const sfmiEnabled = s.sfmi.driverStatus.type === T.FS.DriverStatusType.Enabled return { @@ -44,8 +44,10 @@ const Container = (op: OwnProps) => { openPathInSystemFileManagerDesktop, pathItem, pathItemActionMenu, + setMoveOrCopySource, setPathItemActionMenuView, sfmiEnabled, + showMoveOrCopy, startRename, } }) @@ -53,7 +55,7 @@ const Container = (op: OwnProps) => { const {pathItem, pathItemActionMenu, fileContext, cancelDownload} = data const {setPathItemActionMenuView, download, newFolderRow, openPathInSystemFileManagerDesktop} = data - const {sfmiEnabled, favoriteIgnore, startRename, dismissDownload} = data + const {sfmiEnabled, favoriteIgnore, startRename, dismissDownload, setMoveOrCopySource, showMoveOrCopy} = data const {downloadID, downloadIntent, view} = pathItemActionMenu const username = useCurrentUserState(s => s.username) @@ -272,6 +274,19 @@ const Container = (op: OwnProps) => { ] as const) : [] + const itemMoveOrCopy = layout.moveOrCopy + ? ([ + { + icon: 'iconfont-copy', + onClick: hideAndCancelAfter(() => { + setMoveOrCopySource(path) + showMoveOrCopy(T.FS.getPathParent(path)) + }), + title: 'Copy or move', + }, + ] as const) + : [] + const items: Kb.MenuItems = [ ...itemNewFolder, ...itemChatTeam, @@ -282,6 +297,7 @@ const Container = (op: OwnProps) => { ...itemSendToChat, ...itemSendToApp, ...itemDownload, + ...itemMoveOrCopy, ...itemIgnore, ...itemRename, ...itemArchive, From 2602d1780a4f4649d21faae9a3a5245880faf6fa Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Thu, 12 Mar 2026 10:26:20 -0400 Subject: [PATCH 07/42] fix bailing early updating reactions (#29011) --- shared/constants/chat2/convostate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index bc17191e958d..0e1ce7897644 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -3182,7 +3182,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { get().id }` ) - return + continue } set(s => { const m = s.messageMap.get(targetOrdinal) From 79b9dfc36481638557e4d84806e15c3b5466eb1f Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:37:23 -0400 Subject: [PATCH 08/42] Copy edit exploding error messages, fix snippet for exploders (#29020) * Copy edit exploding error messages, fix snippet for exploders * x * x --- go/chat/localizer.go | 5 +++-- go/chat/utils/utils.go | 19 ++++++++++++++----- go/chat/utils/utils_test.go | 30 ++++++++++++++++++++++++++++++ go/ephemeral/common_test.go | 5 ++++- go/ephemeral/errors.go | 10 +++++----- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/go/chat/localizer.go b/go/chat/localizer.go index af3aa42fb48b..444624b75473 100644 --- a/go/chat/localizer.go +++ b/go/chat/localizer.go @@ -845,8 +845,9 @@ func (s *localizerPipeline) localizeConversation(ctx context.Context, uid gregor var maxValidID chat1.MessageID s.Debug(ctx, "localizing %d max msgs", len(maxMsgs)) for _, mm := range maxMsgs { - if mm.IsValid() && - utils.IsSnippetChatMessageType(mm.GetMessageType()) && + isValidSnippet := mm.IsValid() && utils.IsSnippetChatMessageType(mm.GetMessageType()) + isEphemeralErr := mm.IsError() && mm.Error().IsEphemeral + if (isValidSnippet || isEphemeralErr) && (conversationLocal.Info.SnippetMsg == nil || conversationLocal.Info.SnippetMsg.GetMessageID() < mm.GetMessageID()) { conversationLocal.Info.SnippetMsg = new(chat1.MessageUnboxed) diff --git a/go/chat/utils/utils.go b/go/chat/utils/utils.go index e65d478aa612..36e1042b8723 100644 --- a/go/chat/utils/utils.go +++ b/go/chat/utils/utils.go @@ -1116,10 +1116,13 @@ func formatDuration(dur time.Duration) string { func getMsgSnippetDecoration(msg chat1.MessageUnboxed) chat1.SnippetDecoration { var msgBody chat1.MessageBody - if msg.IsValid() { + switch { + case msg.IsValid(): msgBody = msg.Valid().MessageBody - } else { + case msg.IsOutbox(): msgBody = msg.Outbox().Msg.MessageBody + default: + return chat1.SnippetDecoration_NONE } switch msg.GetMessageType() { case chat1.MessageType_ATTACHMENT: @@ -1220,14 +1223,20 @@ func GetMsgSnippetBody(ctx context.Context, g *globals.Context, uid gregor1.UID, func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg chat1.MessageUnboxed, conv chat1.ConversationLocal, currentUsername string, ) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) { - if !msg.IsValid() && !msg.IsOutbox() { - return chat1.SnippetDecoration_NONE, "", "" - } defer func() { if len(snippetDecorated) == 0 { snippetDecorated = snippet } }() + if !msg.IsValid() && !msg.IsOutbox() { + if msg.IsError() && msg.Error().IsEphemeral { + if msg.Error().IsEphemeralExpired(time.Now()) { + return chat1.SnippetDecoration_EXPLODED_MESSAGE, "Message exploded.", "" + } + return chat1.SnippetDecoration_EXPLODING_MESSAGE, msg.Error().ErrMsg, "" + } + return chat1.SnippetDecoration_NONE, "", "" + } var senderUsername string if msg.IsValid() { diff --git a/go/chat/utils/utils_test.go b/go/chat/utils/utils_test.go index a8b0acdabc0c..e39b1ac63060 100644 --- a/go/chat/utils/utils_test.go +++ b/go/chat/utils/utils_test.go @@ -1086,6 +1086,36 @@ func TestSearchableRemoteConversationName(t *testing.T) { searchableRemoteConversationNameFromStr("joshblum,zoommikem,mikem,zoomua,mikem", "mikem")) } +func TestGetMsgSnippetEphemeralError(t *testing.T) { + ctx := context.Background() + conv := chat1.ConversationLocal{} + errMsg := "This exploding message is not available because this device was created after the message was sent" + + // Non-expired ephemeral error: should surface the error message with EXPLODING_MESSAGE decoration. + msg := chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{ + ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL, + ErrMsg: errMsg, + IsEphemeral: true, + Etime: gregor1.ToTime(time.Now().Add(time.Hour)), + MessageType: chat1.MessageType_TEXT, + }) + decoration, snippet, _ := GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice") + require.Equal(t, chat1.SnippetDecoration_EXPLODING_MESSAGE, decoration) + require.Equal(t, errMsg, snippet) + + // Expired ephemeral error: should show "Message exploded." with EXPLODED_MESSAGE decoration. + msg = chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{ + ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL, + ErrMsg: errMsg, + IsEphemeral: true, + Etime: gregor1.ToTime(time.Now().Add(-time.Hour)), + MessageType: chat1.MessageType_TEXT, + }) + decoration, snippet, _ = GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice") + require.Equal(t, chat1.SnippetDecoration_EXPLODED_MESSAGE, decoration) + require.Equal(t, "Message exploded.", snippet) +} + func TestStripUsernameFromConvName(t *testing.T) { // Only the username as a complete segment is removed; "mikem" inside "zoommikem" must not be stripped require.Equal(t, "joshblum,zoommikem,zoomua", diff --git a/go/ephemeral/common_test.go b/go/ephemeral/common_test.go index dd7473975259..eb2eefdb72be 100644 --- a/go/ephemeral/common_test.go +++ b/go/ephemeral/common_test.go @@ -132,7 +132,10 @@ func TestEphemeralPluralization(t *testing.T) { require.Equal(t, humanMsg, pluralized) pluralized = PluralizeErrorMessage(humanMsg, 2) - require.Equal(t, "2 exploding messages are not available, because this device was created after it was sent", pluralized) + require.Equal(t, "2 exploding messages are not available because this device was created after the messages were sent", pluralized) + + pluralized = PluralizeErrorMessage(humanMsgWithPrefix(MemberAfterEKErrMsg), 3) + require.Equal(t, "3 exploding messages are not available because you joined the team after the messages were sent", pluralized) pluralized = PluralizeErrorMessage(DefaultHumanErrMsg, 2) require.Equal(t, "2 exploding messages are not available", pluralized) diff --git a/go/ephemeral/errors.go b/go/ephemeral/errors.go index 29f0b1ebd009..5bab64893c2b 100644 --- a/go/ephemeral/errors.go +++ b/go/ephemeral/errors.go @@ -82,10 +82,10 @@ func newTransientEphemeralKeyError(err EphemeralKeyError) EphemeralKeyError { const ( DefaultHumanErrMsg = "This exploding message is not available" DefaultPluralHumanErrMsg = "%d exploding messages are not available" - DeviceCloneErrMsg = "cloned devices do not support exploding messages" - DeviceCloneWithOneshotErrMsg = "to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance" - DeviceAfterEKErrMsg = "because this device was created after it was sent" - MemberAfterEKErrMsg = "because you joined the team after it was sent" + DeviceCloneErrMsg = "because this device has been cloned" + 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" + DeviceAfterEKErrMsg = "because this device was created after the message was sent" + MemberAfterEKErrMsg = "because you joined the team after the message was sent" DeviceStaleErrMsg = "because this device wasn't online to generate an exploding key" UserStaleErrMsg = "because you weren't online to generate new exploding keys" ) @@ -159,7 +159,7 @@ func humanMsgWithPrefix(humanMsg string) string { if humanMsg == "" { humanMsg = DefaultHumanErrMsg } else if !strings.Contains(humanMsg, DefaultHumanErrMsg) { - humanMsg = fmt.Sprintf("%s, %s", DefaultHumanErrMsg, humanMsg) + humanMsg = fmt.Sprintf("%s %s", DefaultHumanErrMsg, humanMsg) } return humanMsg } From dcf9c5495dd2d8f2afff2034e5b986e54a9097f5 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:06:42 -0400 Subject: [PATCH 09/42] pin avdl-compiler (#29024) --- protocol/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/package.json b/protocol/package.json index bd5b7938feb8..0452f4851000 100644 --- a/protocol/package.json +++ b/protocol/package.json @@ -17,7 +17,7 @@ }, "homepage": "https://github.com/keybase/protocol", "dependencies": { - "avdl-compiler": "github:keybase/node-avdl-compiler#master", + "avdl-compiler": "github:keybase/node-avdl-compiler#7fe780ac783fa214e2d0f54d8f48ea490e8c1612", "avdl2json": "2.2.5", "camelcase": "6.3.0", "iced-coffee-script": "108.0.14", From de09dcebd061a6d95854cc249d86006535373cc1 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:43:19 -0400 Subject: [PATCH 10/42] additional logging around startup (#29022) * additional logging around startup * fix yarn --- go/bind/keybase.go | 3 ++- protocol/yarn.lock | 2 +- shared/constants/daemon/index.tsx | 5 +++++ shared/ios/Keybase/AppDelegate.swift | 2 ++ shared/router-v2/router.native.tsx | 3 +++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/go/bind/keybase.go b/go/bind/keybase.go index 4c94b7f80d7d..117bf2768bc6 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -524,6 +524,7 @@ func ReadArr() (data []byte, err error) { // ensureConnection establishes the loopback connection if not already connected. // Must be called with connMutex held. func ensureConnection() error { + start := time.Now() if !isInited() { return errors.New("keybase not initialized") } @@ -536,7 +537,7 @@ func ensureConnection() error { if err != nil { return fmt.Errorf("Failed to dial loopback listener: %s", err) } - log("Go: Established loopback connection") + log("Go: Established loopback connection in %v", time.Since(start)) return nil } diff --git a/protocol/yarn.lock b/protocol/yarn.lock index 26b2a5cbad88..3777ca14c466 100644 --- a/protocol/yarn.lock +++ b/protocol/yarn.lock @@ -17,7 +17,7 @@ amdefine@>=0.0.4: resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= -"avdl-compiler@github:keybase/node-avdl-compiler#master": +"avdl-compiler@github:keybase/node-avdl-compiler#7fe780ac783fa214e2d0f54d8f48ea490e8c1612": version "1.4.10" resolved "https://codeload.github.com/keybase/node-avdl-compiler/tar.gz/7fe780ac783fa214e2d0f54d8f48ea490e8c1612" dependencies: diff --git a/shared/constants/daemon/index.tsx b/shared/constants/daemon/index.tsx index 15d3f95b7f9c..c45057ab85ef 100644 --- a/shared/constants/daemon/index.tsx +++ b/shared/constants/daemon/index.tsx @@ -107,8 +107,11 @@ export const useDaemonState = Z.createZustand((set, get) => { const name = 'config.getBootstrapStatus' const {wait} = get().dispatch wait(name, version, true) + const t = Date.now() + logger.info('[Bootstrap] loadDaemonBootstrapStatus: starting') try { await get().dispatch.loadDaemonBootstrapStatus() + logger.info(`[Bootstrap] loadDaemonBootstrapStatus: done in ${Date.now() - t}ms`) storeRegistry.getState('dark-mode').dispatch.loadDarkPrefs() storeRegistry.getState('chat').dispatch.loadStaticConfig() } finally { @@ -303,6 +306,8 @@ export const useDaemonState = Z.createZustand((set, get) => { s.handshakeWaiters.set(name, newCount) } }) + const remaining = get().handshakeWaiters.size + logger.info(`[Bootstrap] waiter ${increment ? '+' : '-'} ${name} v${version}, remaining: ${remaining}`) if (failedFatal) { get().dispatch.setFailed(failedReason || '') diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 1d6a47878c70..29fe3a34c356 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -408,6 +408,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationDidBecomeActive(_ application: UIApplication) { + let elapsed = CFAbsoluteTimeGetCurrent() - AppDelegate.appStartTime + writeStartupTimingLog(String(format: "applicationDidBecomeActive: %.1fms after launch", elapsed * 1000)) NSLog("applicationDidBecomeActive: hiding keyz screen.") hideCover() NSLog("applicationDidBecomeActive: notifying service.") diff --git a/shared/router-v2/router.native.tsx b/shared/router-v2/router.native.tsx index f807285c2920..670f99bfc72e 100644 --- a/shared/router-v2/router.native.tsx +++ b/shared/router-v2/router.native.tsx @@ -182,6 +182,9 @@ const RNApp = React.memo(function RNApp() { const rootKey = Hooks.useRootKey() if (initialStateState !== 'loaded' || !loggedInLoaded) { + logger.info( + `[Router] showing SimpleLoading: initialStateState=${initialStateState} loggedInLoaded=${loggedInLoaded}` + ) return ( From 8b8c50f4cff8cf6ff51e348221bb1544905d32d0 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 17 Mar 2026 22:01:47 -0400 Subject: [PATCH 11/42] Fix git clone failing when default branch is not master (#29004) * Fix git clone failing when default branch is not master When a user pushes only a non-master branch (e.g. main) to a Keybase git repo, cloning fails with "remote HEAD refers to nonexistent ref, unable to checkout" because gogit.Init() hardcodes HEAD to refs/heads/master. Three fixes: - handleList: validate symref targets exist, rewrite HEAD to best available branch (prefer main > master > alphabetical first) - handlePushBatch: after push, update stored HEAD if it points to a nonexistent ref (skipping delete refspecs) - NewBrowser: resolve HEAD dynamically instead of hardcoding master, with fallback to empty browser for stale HEAD Fixes #28943 * Fix PR #29004 feedback: deterministic HEAD repair, journal flush, and test - Use main > master > alphabetical preference for HEAD repair branch selection instead of nondeterministic map iteration order - Add waitForJournal after HEAD repair so the update is flushed - Add TestBrowserHeadResolution covering non-master HEAD and stale HEAD * Address PR #29004 feedback: extract helper, fix flush ordering, and harden test - Extract bestBranchFromCandidates helper to deduplicate HEAD selection logic - Move HEAD fixup before waitForJournal so the update is included in the flush - Iterate args slice instead of refspecs map for deterministic branch selection - Propagate journal flush error instead of swallowing it - Use git checkout -B (idempotent) to avoid flakiness with init.defaultBranch - Add assertion verifying the underlying KBFS repo HEAD symref is updated * Use all repo branches (not just pushed refs) for HEAD fixup in handlePushBatch The HEAD fixup was only considering refs from the current push batch when selecting the best branch. This could pick a suboptimal branch if better candidates (e.g. main) already existed in the repo. Now enumerates all repo branches, consistent with how handleList selects the best HEAD target. * Fix doc comment placement, close reference iterators - Move bestBranchFromCandidates above handleList so its doc comment is not split - Add defer refs.Close() in handleList - Add defer allRefs.Close() in handlePushBatch HEAD fixup * Scope symref rewrite to HEAD only, update NewBrowser doc comment * Fix NewBrowser doc comment to match fallback behavior * Fall back to master when HEAD points to a stale ref in NewBrowser * Emit non-HEAD symrefs unchanged when their targets are unknown * Move bestBranch declaration before error check in handlePushBatch --------- Co-authored-by: Chris Nojima --- go/kbfs/kbfsgit/runner.go | 108 ++++++++++++++++++++++++++++++++- go/kbfs/kbfsgit/runner_test.go | 42 ++++++++++++- go/kbfs/libgit/browser.go | 29 +++++++-- go/kbfs/libgit/browser_test.go | 66 ++++++++++++++++++++ 4 files changed, 235 insertions(+), 10 deletions(-) diff --git a/go/kbfs/kbfsgit/runner.go b/go/kbfs/kbfsgit/runner.go index f9a60c26d9f0..c4551957f393 100644 --- a/go/kbfs/kbfsgit/runner.go +++ b/go/kbfs/kbfsgit/runner.go @@ -682,6 +682,21 @@ func (r *runner) waitForJournal(ctx context.Context) error { r.printStageEndIfNeeded) } +// bestBranchFromCandidates selects the best branch for HEAD from a set of +// candidate branch names (prefer main > master > alphabetically first). +func bestBranchFromCandidates(best plumbing.ReferenceName, candidate plumbing.ReferenceName) plumbing.ReferenceName { + switch { + case candidate == "refs/heads/main": + return candidate + case candidate == "refs/heads/master" && best != "refs/heads/main": + return candidate + case best == "" || (best != "refs/heads/main" && best != "refs/heads/master" && candidate < best): + return candidate + default: + return best + } +} + // handleList: From https://git-scm.com/docs/git-remote-helpers // // Lists the refs, one per line, in the format " [ @@ -708,9 +723,18 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) { if err != nil { return err } + defer refs.Close() - var symRefs []string + type symRefInfo struct { + name plumbing.ReferenceName + target plumbing.ReferenceName + } + var symRefs []symRefInfo + hashRefNames := make(map[plumbing.ReferenceName]bool) hashesSeen := false + // Track the best fallback branch for HEAD in case its target + // doesn't exist (prefer main > master > alphabetically first). + var bestBranch plumbing.ReferenceName for { ref, err := refs.Next() if errors.Cause(err) == io.EOF { @@ -725,6 +749,11 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) { case plumbing.HashReference: value = ref.Hash().String() hashesSeen = true + hashRefNames[ref.Name()] = true + // Track best branch for fallback HEAD. + if strings.HasPrefix(ref.Name().String(), "refs/heads/") { + bestBranch = bestBranchFromCandidates(bestBranch, ref.Name()) + } case plumbing.SymbolicReference: value = "@" + ref.Target().String() default: @@ -737,7 +766,10 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) { // cloning an empty repo will result in an error because // the HEAD symbolic ref points to a ref that doesn't // exist. - symRefs = append(symRefs, refStr) + symRefs = append(symRefs, symRefInfo{ + name: ref.Name(), + target: ref.Target(), + }) continue } r.log.CDebugf(ctx, "Listing ref %s", refStr) @@ -748,7 +780,30 @@ func (r *runner) handleList(ctx context.Context, args []string) (err error) { } if hashesSeen && !forPush { - for _, refStr := range symRefs { + for _, sr := range symRefs { + target := sr.target + // If the symref target doesn't exist among hash refs, + // rewrite it to point to the best available branch, + // but only for HEAD. Other symrefs are emitted as-is. + if !hashRefNames[target] { + if sr.name == plumbing.HEAD { + if bestBranch == "" { + r.log.CDebugf(ctx, + "Skipping HEAD symref %s -> %s (no branches available)", + sr.name, target) + continue + } + r.log.CDebugf(ctx, + "Rewriting HEAD symref from %s to %s", + target, bestBranch) + target = bestBranch + } else { + r.log.CDebugf(ctx, + "Emitting non-HEAD symref %s -> %s with unknown target", + sr.name, target) + } + } + refStr := "@" + target.String() + " " + sr.name.String() + "\n" r.log.CDebugf(ctx, "Listing symbolic ref %s", refStr) _, err = r.output.Write([]byte(refStr)) if err != nil { @@ -1830,6 +1885,53 @@ func (r *runner) handlePushBatch(ctx context.Context, args [][]string) ( return nil, err } + // If HEAD points to a nonexistent ref, update it to point to the + // best available branch in the repo (prefer main > master > alphabetical). + // We intentionally repair HEAD even when the target was explicitly + // deleted in this batch, because a broken HEAD causes clone failures. + // This must happen before waitForJournal so the HEAD update is + // included in the same flush. + head, headErr := repo.Storer.Reference(plumbing.HEAD) + if headErr == nil && head.Type() == plumbing.SymbolicReference { + _, targetErr := repo.Storer.Reference(head.Target()) + var bestBranch plumbing.ReferenceName + if targetErr == plumbing.ErrReferenceNotFound { + allRefs, refsErr := repo.References() + if refsErr == nil { + defer allRefs.Close() + for { + ref, nextErr := allRefs.Next() + if errors.Cause(nextErr) == io.EOF { + break + } + if nextErr != nil { + break + } + if ref.Type() != plumbing.HashReference { + continue + } + if !strings.HasPrefix(ref.Name().String(), "refs/heads/") { + continue + } + bestBranch = bestBranchFromCandidates(bestBranch, ref.Name()) + } + } + } + if bestBranch != "" { + newHead := plumbing.NewSymbolicReference( + plumbing.HEAD, bestBranch) + if setErr := repo.Storer.SetReference( + newHead); setErr != nil { + r.log.CDebugf(ctx, + "Error updating HEAD to %s: %+v", + bestBranch, setErr) + } else { + r.log.CDebugf(ctx, + "Updated HEAD to point to %s", bestBranch) + } + } + } + err = r.waitForJournal(ctx) if err != nil { return nil, err diff --git a/go/kbfs/kbfsgit/runner_test.go b/go/kbfs/kbfsgit/runner_test.go index 982994338f1c..3648bb44dbb2 100644 --- a/go/kbfs/kbfsgit/runner_test.go +++ b/go/kbfs/kbfsgit/runner_test.go @@ -27,6 +27,7 @@ import ( "github.com/keybase/client/go/protocol/keybase1" "github.com/stretchr/testify/require" gogitcfg "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing" ) type testErrput struct { @@ -156,7 +157,7 @@ func makeLocalRepoWithOneFileCustomCommitMsg(t *testing.T, gitExec(t, dotgit, gitDir, "init") if branch != "" { - gitExec(t, dotgit, gitDir, "checkout", "-b", branch) + gitExec(t, dotgit, gitDir, "checkout", "-B", branch) } gitExec(t, dotgit, gitDir, "add", filename) @@ -1289,3 +1290,42 @@ func TestRunnerLFS(t *testing.T) { require.NoError(t, err) require.Equal(t, lfsData, buf) } + +// Test that when only a non-master branch (e.g. "main") is pushed, +// HEAD correctly points to that branch instead of the nonexistent +// "refs/heads/master". +func TestRunnerListNonMasterDefault(t *testing.T) { + ctx, config, tempdir := initConfigForRunner(t) + defer func() { _ = os.RemoveAll(tempdir) }() + defer libkbfs.CheckConfigAndShutdown(ctx, t, config) + + gitDir, err := os.MkdirTemp(os.TempDir(), "kbfsgittest") + require.NoError(t, err) + defer func() { _ = os.RemoveAll(gitDir) }() + + makeLocalRepoWithOneFile(t, gitDir, "foo", "hello", "main") + + h, err := tlfhandle.ParseHandle( + ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private) + require.NoError(t, err) + _, err = libgit.CreateRepoAndID(ctx, config, h, "test") + require.NoError(t, err) + + testPush(ctx, t, config, gitDir, + "refs/heads/main:refs/heads/main") + + // Verify the underlying KBFS repo HEAD symref was actually updated. + fs, _, err := libgit.GetRepoAndID(ctx, config, h, "test", "") + require.NoError(t, err) + storage, err := libgit.NewGitConfigWithoutRemotesStorer(fs) + require.NoError(t, err) + headRef, err := storage.Reference(plumbing.HEAD) + require.NoError(t, err) + require.Equal(t, plumbing.SymbolicReference, headRef.Type()) + require.Equal(t, plumbing.ReferenceName("refs/heads/main"), headRef.Target()) + + // List refs and verify HEAD points to refs/heads/main. + heads := testListAndGetHeads(ctx, t, config, gitDir, + []string{"refs/heads/main", "HEAD"}) + require.Equal(t, "@refs/heads/main", heads[1]) +} diff --git a/go/kbfs/libgit/browser.go b/go/kbfs/libgit/browser.go index 8f4c7dd7160c..4b3b9fb5bf22 100644 --- a/go/kbfs/libgit/browser.go +++ b/go/kbfs/libgit/browser.go @@ -57,10 +57,12 @@ type Browser struct { var _ billy.Filesystem = (*Browser)(nil) // NewBrowser makes a new Browser instance, browsing the given branch -// of the given repo. If `gitBranchName` is empty, -// "refs/heads/master" is used. If `gitBranchName` is not empty, but -// it doesn't begin with "refs/", then "refs/heads/" is prepended to -// it. +// of the given repo. If `gitBranchName` is empty, HEAD is resolved +// to determine the default branch; if HEAD is missing or points to a +// nonexistent ref, NewBrowser falls back to "refs/heads/master" if it +// exists, otherwise the repo is treated as empty. If `gitBranchName` +// is not empty but doesn't begin with "refs/", then "refs/heads/" is +// prepended to it. func NewBrowser( repoFS *libfs.FS, clock libkbfs.Clock, gitBranchName plumbing.ReferenceName, @@ -73,6 +75,7 @@ func NewBrowser( } const masterBranch = "refs/heads/master" + branchWasEmpty := gitBranchName == "" if gitBranchName == "" { gitBranchName = masterBranch } else if !strings.HasPrefix(string(gitBranchName), "refs/") { @@ -98,9 +101,23 @@ func NewBrowser( return nil, err } + // If no branch was specified, try to resolve HEAD to find the + // default branch instead of hardcoding master. + if branchWasEmpty { + headRef, headErr := repo.Reference(plumbing.HEAD, false) + if headErr == nil && headRef.Type() == plumbing.SymbolicReference { + gitBranchName = headRef.Target() + } + } + ref, err := repo.Reference(gitBranchName, true) - if err == plumbing.ErrReferenceNotFound && gitBranchName == masterBranch { - // This branch has no commits, so pretend it's empty. + if err == plumbing.ErrReferenceNotFound && branchWasEmpty && gitBranchName != masterBranch { + // HEAD points to a nonexistent ref; fall back to master. + gitBranchName = masterBranch + ref, err = repo.Reference(gitBranchName, true) + } + if err == plumbing.ErrReferenceNotFound && (gitBranchName == masterBranch || branchWasEmpty) { + // No commits on this branch, so pretend it's empty. return &Browser{ root: string(gitBranchName), sharedCache: sharedCache, diff --git a/go/kbfs/libgit/browser_test.go b/go/kbfs/libgit/browser_test.go index 480d9abd8841..5c2120f1333d 100644 --- a/go/kbfs/libgit/browser_test.go +++ b/go/kbfs/libgit/browser_test.go @@ -19,6 +19,7 @@ import ( "github.com/keybase/client/go/protocol/keybase1" "github.com/stretchr/testify/require" gogit "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" ) @@ -176,3 +177,68 @@ func TestBrowserWithCache(t *testing.T) { require.NoError(t, err) testBrowser(t, cache) } + +func TestBrowserHeadResolution(t *testing.T) { + ctx, config, cancel, tempdir := initConfigForAutogit(t) + defer cancel() + defer func() { _ = os.RemoveAll(tempdir) }() + defer libkbfs.CheckConfigAndShutdown(ctx, t, config) + + h, err := tlfhandle.ParseHandle( + ctx, config.KBPKI(), config.MDOps(), nil, "user1", tlf.Private) + require.NoError(t, err) + rootFS, err := libfs.NewFS( + ctx, config, h, data.MasterBranch, "", "", keybase1.MDPriorityNormal) + require.NoError(t, err) + + t.Log("Init a new repo directly into KBFS.") + dotgitFS, _, err := GetOrCreateRepoAndID(ctx, config, h, "test-head", "") + require.NoError(t, err) + + err = rootFS.MkdirAll("worktree-head", 0o600) + require.NoError(t, err) + worktreeFS, err := rootFS.Chroot("worktree-head") + require.NoError(t, err) + dotgitStorage, err := NewGitConfigWithoutRemotesStorer(dotgitFS) + require.NoError(t, err) + repo, err := gogit.Init(dotgitStorage, worktreeFS) + require.NoError(t, err) + + t.Log("Set HEAD to point to refs/heads/main instead of master.") + newHead := plumbing.NewSymbolicReference( + plumbing.HEAD, "refs/heads/main") + err = repo.Storer.SetReference(newHead) + require.NoError(t, err) + + t.Log("Commit a file — go-git creates the main branch since HEAD points there.") + addFileToWorktreeAndCommit( + ctx, t, config, h, repo, worktreeFS, "hello.txt", "world") + + t.Log("NewBrowser with empty branch should resolve HEAD to main.") + b, err := NewBrowser(dotgitFS, config.Clock(), "", noopSharedInBrowserCache{}) + require.NoError(t, err) + require.NotNil(t, b.tree, "browser should have a non-nil tree") + fi, err := b.Stat("hello.txt") + require.NoError(t, err) + require.Equal(t, "hello.txt", fi.Name()) + + f, err := b.Open("hello.txt") + require.NoError(t, err) + defer func() { _ = f.Close() }() + fileData, err := io.ReadAll(f) + require.NoError(t, err) + require.Equal(t, "world", string(fileData)) + + t.Log("Test stale HEAD: point HEAD to a nonexistent ref.") + staleHead := plumbing.NewSymbolicReference( + plumbing.HEAD, "refs/heads/nonexistent") + err = repo.Storer.SetReference(staleHead) + require.NoError(t, err) + + t.Log("NewBrowser should return an empty browser without error.") + b, err = NewBrowser(dotgitFS, config.Clock(), "", noopSharedInBrowserCache{}) + require.NoError(t, err) + fis, err := b.ReadDir("") + require.NoError(t, err) + require.Len(t, fis, 0) +} From 898b99bdcfbbb6c80197d2b6785c033990ed7dec Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 18 Mar 2026 09:46:26 -0400 Subject: [PATCH 12/42] changelog (#29027) --- shared/desktop/CHANGELOG.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/shared/desktop/CHANGELOG.txt b/shared/desktop/CHANGELOG.txt index 94f072196ab0..b3e1f41a1943 100644 --- a/shared/desktop/CHANGELOG.txt +++ b/shared/desktop/CHANGELOG.txt @@ -1,6 +1,4 @@ -• On iOS quickly share to recent conversations -• Emoji 16 support -• iOS HEIC Avatar support -• Better sharing/push support -• Various bug fixes and performance improvements +• Improve git default branch handling +• Fix widget opening into chat search mode +• Fix copy/move menu in KBFS From 3898f2693ebe061b4fac04c861ac4dbaf0fd4aff Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:13:25 -0400 Subject: [PATCH 13/42] upgrade xpath (#29028) --- go/go.mod | 46 +++++++------- go/go.sum | 183 +++++++++++++++++++++++++----------------------------- 2 files changed, 106 insertions(+), 123 deletions(-) diff --git a/go/go.mod b/go/go.mod index 1c0c4af9b7d4..38a719ab678c 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,13 +1,13 @@ module github.com/keybase/client/go -go 1.24.0 +go 1.25.0 toolchain go1.25.5 require ( bazil.org/fuse v0.0.0-20200424023519-3c101025617f camlistore.org v0.0.0-20161205184337-c55c8602d3ce - github.com/PuerkitoBio/goquery v1.5.1 + github.com/PuerkitoBio/goquery v1.12.0 github.com/akavel/rsrc v0.2.1-0.20151103204339-ba14da1f8271 github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e github.com/blang/semver v3.5.1+incompatible @@ -25,7 +25,7 @@ require ( github.com/gammazero/workerpool v0.0.0-20181230203049-86a96b5d5d92 github.com/go-errors/errors v1.4.2 github.com/go-sql-driver/mysql v1.9.3 - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 github.com/golang/mock v1.6.0 github.com/hashicorp/golang-lru v0.5.4 github.com/josephspurrier/goversioninfo v0.0.0-20160622020813-53f6213da3d7 @@ -71,13 +71,13 @@ require ( github.com/urfave/cli v1.22.1 github.com/vividcortex/ewma v1.1.2-0.20170804035156-43880d236f69 go.uber.org/zap v1.24.0 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.49.0 golang.org/x/image v0.34.0 golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 // indirect - golang.org/x/net v0.48.0 - golang.org/x/sync v0.19.0 - golang.org/x/sys v0.39.0 - golang.org/x/text v0.32.0 + golang.org/x/net v0.52.0 + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.42.0 + golang.org/x/text v0.35.0 golang.org/x/time v0.14.0 gopkg.in/src-d/go-billy.v4 v4.3.2 gopkg.in/src-d/go-git.v4 v4.13.1 @@ -91,7 +91,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.32.6 github.com/aws/aws-sdk-go-v2/config v1.28.6 github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 - github.com/gocolly/colly/v2 v2.1.1-0.20231020184023-3c987f1982ed + github.com/gocolly/colly/v2 v2.3.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a gopkg.in/alecthomas/kingpin.v2 v2.2.6 @@ -104,11 +104,11 @@ require ( github.com/StackExchange/wmi v1.2.1 // indirect github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect - github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect - github.com/antchfx/htmlquery v1.2.3 // indirect - github.com/antchfx/xmlquery v1.3.4 // indirect - github.com/antchfx/xpath v1.1.10 // indirect + github.com/antchfx/htmlquery v1.3.6 // indirect + github.com/antchfx/xmlquery v1.5.0 // indirect + github.com/antchfx/xpath v1.3.6 // indirect github.com/asaskevich/govalidator v0.0.0-20180319081651-7d2e70ef918f // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect @@ -125,7 +125,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect github.com/aws/smithy-go v1.22.1 // indirect - github.com/bits-and-blooms/bitset v1.2.2-0.20220111210104-dfa3e347c392 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/segment v0.8.0 // indirect @@ -142,7 +142,7 @@ require ( github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -156,14 +156,14 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect github.com/nf/cr2 v0.0.0-20140528043846-05d46fef4f2f // indirect - github.com/nlnwa/whatwg-url v0.1.2 // indirect + github.com/nlnwa/whatwg-url v0.6.2 // indirect github.com/onsi/gomega v1.36.2 // indirect github.com/pelletier/go-buffruneio v0.3.0 // indirect github.com/philhofer/fwd v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/rwcarlsen/goexif v0.0.0-20150520140647-709fab3d192d // indirect - github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/simplereach/timeutils v1.2.0 // indirect @@ -174,7 +174,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/strib/gomounts v0.0.0-20180215003523-d9ea4eaa52ca // indirect github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect - github.com/temoto/robotstxt v1.1.1 // indirect + github.com/temoto/robotstxt v1.1.2 // indirect github.com/tinylib/msgp v1.1.0 // indirect github.com/willf/bitset v1.1.11-0.20190404145324-77892cd8d53f // indirect github.com/xanzy/ssh-agent v0.2.0 // indirect @@ -182,12 +182,12 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go4.org v0.0.0-20161118210015-09d86de304dc // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect + golang.org/x/tools v0.42.0 // indirect golang.org/x/vuln v1.1.4 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 // indirect diff --git a/go/go.sum b/go/go.sum index 98b6b4b68162..6d01e32bfabe 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,4 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.2.1-0.20160717150709-99064174e013/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -6,8 +5,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Masterminds/squirrel v0.0.0-20161115235646-20f192218cf5/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM= -github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/RoaringBitmap/roaring v0.4.22-0.20191112221735-4d53b29a8f7d h1:AaTEP55Sj0H3etCU0/Hr4uJNh42T5jRlU/r1dteKH8I= github.com/RoaringBitmap/roaring v0.4.22-0.20191112221735-4d53b29a8f7d/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= @@ -23,18 +22,17 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M= -github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0= -github.com/antchfx/xmlquery v1.3.4 h1:RuhsI4AA5Ma4XoXhaAr2VjJxU0Xp0W2zy/f9ZIpsF4s= -github.com/antchfx/xmlquery v1.3.4/go.mod h1:64w0Xesg2sTaawIdNqMB+7qaW/bSqkQm+ssPaCMWNnc= -github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= -github.com/antchfx/xpath v1.1.10 h1:cJ0pOvEdN/WvYXxvRrzQH9x5QWKpzHacYO8qzCcDYAg= -github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/antchfx/htmlquery v1.3.6 h1:RNHHL7YehO5XdO8IM8CynwLKONwRHWkrghbYhQIk9ag= +github.com/antchfx/htmlquery v1.3.6/go.mod h1:kcVUqancxPygm26X2rceEcagZFFVkLEE7xgLkGSDl/4= +github.com/antchfx/xmlquery v1.5.0 h1:uAi+mO40ZWfyU6mlUBxRVvL6uBNZ6LMU4M3+mQIBV4c= +github.com/antchfx/xmlquery v1.5.0/go.mod h1:lJfWRXzYMK1ss32zm1GQV3gMIW/HFey3xDZmkP1SuNc= +github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= +github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e h1:s05JG2GwtJMHaPcXDpo4V35TFgyYZzNsmBlSkHPEbeg= github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -79,8 +77,9 @@ github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bits-and-blooms/bitset v1.2.2-0.20220111210104-dfa3e347c392 h1:9d7ak0NpT8/bhFM5ZkQuLpeS8Ey9zDY9OJJcOYqYV4c= -github.com/bits-and-blooms/bitset v1.2.2-0.20220111210104-dfa3e347c392/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blevesearch/bleve v0.8.2-0.20191030071327-189ee421f71e h1:JCcMFXeEvelJX6uSfSNiReo//1ukxugA/yQT2J2iXuM= @@ -95,11 +94,9 @@ github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoA github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -135,8 +132,6 @@ github.com/edsrzf/mmap-go v1.0.1-0.20190108065903-904c4ced31cd/go.mod h1:W3m91qe github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/emirpasic/gods v1.12.1-0.20181020102604-7c131f671417 h1:YxPhcuk2uFv51gUaZnDTK4za30/5kY6IJJ2GSR7e/LY= github.com/emirpasic/gods v1.12.1-0.20181020102604-7c131f671417/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/inject v0.0.0-20161006174721-cc1aa653e50f/go.mod h1:oO8UHw+fDHjDsk4CTy/E96WDzFUYozAtBAaGNoVL0+c= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= @@ -176,44 +171,39 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/packr v1.12.1/go.mod h1:H2dZhQFqHeZwr/5A/uGQkBp7xYuMGuzXFeKhYdcz5No= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe h1:zn8tqiUbec4wR94o7Qj3LZCAT6uGobhEgnDRg6isG5U= github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gocolly/colly/v2 v2.1.1-0.20231020184023-3c987f1982ed h1:JBVpXGF611yz+zSXdRbWOqZ4C6gs7YpthVvm752yO4Q= -github.com/gocolly/colly/v2 v2.1.1-0.20231020184023-3c987f1982ed/go.mod h1:bpukTX2Y+tFDoVBr4gAh7osKn/IbhWTgdmL1sMP0u0c= +github.com/gocolly/colly/v2 v2.3.0 h1:HSFh0ckbgVd2CSGRE+Y/iA4goUhGROJwyQDCMXGFBWM= +github.com/gocolly/colly/v2 v2.3.0/go.mod h1:Qp54s/kQbwCQvFVx8KzKCSTXVJ1wWT4QeAKEu33x1q8= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= @@ -238,7 +228,6 @@ github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/C github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM= github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= -github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -365,8 +354,8 @@ github.com/nf/cr2 v0.0.0-20140528043846-05d46fef4f2f h1:nyKdx+jcykIdxGNrbgo/TGjd github.com/nf/cr2 v0.0.0-20140528043846-05d46fef4f2f/go.mod h1:HazDB3gS/i//QXMMRmTAV7Ni9gAi4mDNTH2HjZ5aVgU= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nlnwa/whatwg-url v0.1.2 h1:BqqsIVG6xv71wOoMAoFDmV6OK6/2sXn7BJdOsTkBl88= -github.com/nlnwa/whatwg-url v0.1.2/go.mod h1:b0r+dEyM/KztLMDSVY6ApcO9Fmzgq+e9+Ugq20UBYck= +github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q= +github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk= github.com/nullstyle/go-xdr v0.0.0-20180726165426-f4c839f75077/go.mod h1:sZZi9x5aHXGZ/RRp7Ne5rkvtDxZb7pd7vgVA+gmE35A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -399,7 +388,6 @@ github.com/pkg/xattr v0.2.2 h1:aOMVxbdr71YJkqrPXbwZRtKNbxvIOiDl3OjzAQbMTT0= github.com/pkg/xattr v0.2.2/go.mod h1:Y9LTXzFU+ntVswypGeJ916t7Haa8ao/kfcTKFEUX/Cs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/qrtz/nativemessaging v0.0.0-20161221035708-f4769a80e040 h1:iKwSrA9BD8ywmJGJy2JbiscEaRrtVY+m/U+ikacZazI= github.com/qrtz/nativemessaging v0.0.0-20161221035708-f4769a80e040/go.mod h1:oJXjZgmJiNwg5XtEvky9JCZqd7CZI7iz+mwmTxjIpak= github.com/rcrowley/go-metrics v0.0.0-20160113235030-51425a2415d2/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -420,8 +408,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20150520140647-709fab3d192d h1:0/OVHHIrQqGZxHRbrX3tJx95MkIbzV44KE66ZGkhH+0= github.com/rwcarlsen/goexif v0.0.0-20150520140647-709fab3d192d/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sebest/xff v0.0.0-20150611211316-7a36e3a787b5/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= @@ -463,7 +451,6 @@ github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible/g github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 h1:JNEGSiWg6D3lcBCMCBqN3ELniXujt+0QNHLhNnO0w3s= github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2/go.mod h1:mjqs7N0Q6m5HpR7QfXVBZXZWSqTjQLeTujjA/xUp2uw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -478,8 +465,8 @@ github.com/strib/gomounts v0.0.0-20180215003523-d9ea4eaa52ca h1:wVJqmi/uGy9ZBcMl github.com/strib/gomounts v0.0.0-20180215003523-d9ea4eaa52ca/go.mod h1:kyoAB93nwIsDbBftMJ8L2vIIGTdNORUr9hwjX83liiA= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= -github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA= -github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= +github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= @@ -536,63 +523,62 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 h1:Cr6kbEvA6nqvdHynE4CtVKlqpZB9dS1Jva/6IsHA19g= golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294/go.mod h1:RdZ+3sb4CVgpCFnzv+I4haEpwqFfsfzlLHs3L7ok+e0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -623,36 +609,45 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= -golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -660,8 +655,10 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -673,31 +670,19 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -728,8 +713,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= perkeep.org v0.0.0-20161205184337-c55c8602d3ce h1:xy8y59WZpw0hhw4yw28PeIKmKorPHjbtPQoHnslwNZQ= From e77ea8e230d576042e95a7d72cb4a40ebd4e95d1 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:54:58 -0400 Subject: [PATCH 14/42] gitignore watchman cookie (#29031) --- shared/.gitignore | 1 + shared/.watchman-cookie-MBP-2019.local-559-1506 | 0 2 files changed, 1 insertion(+) delete mode 100755 shared/.watchman-cookie-MBP-2019.local-559-1506 diff --git a/shared/.gitignore b/shared/.gitignore index cef9c5e90b68..114613c2f058 100644 --- a/shared/.gitignore +++ b/shared/.gitignore @@ -55,3 +55,4 @@ main.jsbundle coverage-ts temp.log +.watchman-cookie-* diff --git a/shared/.watchman-cookie-MBP-2019.local-559-1506 b/shared/.watchman-cookie-MBP-2019.local-559-1506 deleted file mode 100755 index e69de29bb2d1..000000000000 From 1f196bf4a4ebde3a54ae38ce23a933318b275d54 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:42:31 -0400 Subject: [PATCH 15/42] set 100ms timeout on Linking.getInitialURL, more logs (#29032) * set 100ms timeout on Linking.getInitialURL, more logs * x --- .../platform-specific/index.native.tsx | 17 ++++++++++++++++- shared/router-v2/hooks.native.tsx | 5 ++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/shared/constants/platform-specific/index.native.tsx b/shared/constants/platform-specific/index.native.tsx index 2c8f08624117..338983951fdc 100644 --- a/shared/constants/platform-specific/index.native.tsx +++ b/shared/constants/platform-specific/index.native.tsx @@ -137,6 +137,8 @@ export const showShareActionSheet = async (options: { } const loadStartupDetails = async () => { + logger.info('[Startup] loadStartupDetails: starting') + const t = Date.now() const [routeState, initialUrl, push] = await Promise.all([ neverThrowPromiseFunc(async () => { try { @@ -146,7 +148,19 @@ const loadStartupDetails = async () => { return Promise.resolve('') } }), - neverThrowPromiseFunc(async () => Linking.getInitialURL()), + neverThrowPromiseFunc(async () => { + const linkingStart = Date.now() + logger.info('[Startup] loadStartupDetails: calling Linking.getInitialURL') + const timeout = new Promise(resolve => setTimeout(() => resolve(null), 100)) + const url = await Promise.race([Linking.getInitialURL(), timeout]) + const elapsed = Date.now() - linkingStart + if (url === null) { + logger.warn(`[Startup] loadStartupDetails: Linking.getInitialURL returned null/timed out in ${elapsed}ms`) + } else { + logger.info(`[Startup] loadStartupDetails: Linking.getInitialURL returned in ${elapsed}ms: ${url}`) + } + return url + }), neverThrowPromiseFunc(getStartupDetailsFromInitialPush), ] as const) @@ -192,6 +206,7 @@ const loadStartupDetails = async () => { tab = '' } + logger.info(`[Startup] loadStartupDetails: done in ${Date.now() - t}ms`) storeRegistry.getState('config').dispatch.setStartupDetails({ conversation: conversation ?? noConversationIDKey, followUser, diff --git a/shared/router-v2/hooks.native.tsx b/shared/router-v2/hooks.native.tsx index e957781818b8..6e51de3c3e3e 100644 --- a/shared/router-v2/hooks.native.tsx +++ b/shared/router-v2/hooks.native.tsx @@ -7,6 +7,7 @@ import {useDeepLinksState} from '@/constants/deeplinks' import {Linking} from 'react-native' import {useColorScheme} from 'react-native' import {usePushState} from '@/constants/push' +import logger from '@/logger' type InitialStateState = 'init' | 'loading' | 'loaded' @@ -96,7 +97,8 @@ export const useInitialState = (loggedInLoaded: boolean) => { } setInitialStateState('loading') const loadInitialURL = async () => { - let url = await Linking.getInitialURL() + const timeout = new Promise(resolve => setTimeout(() => resolve(null), 100)) + let url = await Promise.race([Linking.getInitialURL(), timeout]) // don't try and resume or follow links if we're signed out if (!loggedIn) { @@ -177,6 +179,7 @@ export const useInitialState = (loggedInLoaded: boolean) => { const f = async () => { await loadInitialURL() + logger.info('[Router] initialStateState loaded, rendering app') setInitialStateState('loaded') } From 510a5f7bac37b99b4f42866112b0d810faa99f86 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:28:07 -0400 Subject: [PATCH 16/42] notifyJSReady logging (#29036) --- rnmodules/react-native-kb/ios/Kb.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index 6bb4193f51ea..8494d25892b5 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -322,7 +322,9 @@ - (NSDictionary *)getConstants { RCT_EXPORT_METHOD(notifyJSReady) { __weak __typeof__(self) weakSelf = self; + NSLog(@"notifyJSReady: called from JS, queuing main thread block"); dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"notifyJSReady: main thread block executing"); // Setup infrastructure [[NSNotificationCenter defaultCenter] addObserver:self From 199f6a657c4335609a4500ceff541e9ac25b8baf Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:59:04 -0400 Subject: [PATCH 17/42] remvoe timout on Linking.getInitialURL (#29037) --- shared/constants/platform-specific/index.native.tsx | 5 ++--- shared/router-v2/hooks.native.tsx | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/shared/constants/platform-specific/index.native.tsx b/shared/constants/platform-specific/index.native.tsx index 338983951fdc..673b778aa441 100644 --- a/shared/constants/platform-specific/index.native.tsx +++ b/shared/constants/platform-specific/index.native.tsx @@ -151,11 +151,10 @@ const loadStartupDetails = async () => { neverThrowPromiseFunc(async () => { const linkingStart = Date.now() logger.info('[Startup] loadStartupDetails: calling Linking.getInitialURL') - const timeout = new Promise(resolve => setTimeout(() => resolve(null), 100)) - const url = await Promise.race([Linking.getInitialURL(), timeout]) + const url = await Linking.getInitialURL() const elapsed = Date.now() - linkingStart if (url === null) { - logger.warn(`[Startup] loadStartupDetails: Linking.getInitialURL returned null/timed out in ${elapsed}ms`) + logger.warn(`[Startup] loadStartupDetails: Linking.getInitialURL returned null in ${elapsed}ms`) } else { logger.info(`[Startup] loadStartupDetails: Linking.getInitialURL returned in ${elapsed}ms: ${url}`) } diff --git a/shared/router-v2/hooks.native.tsx b/shared/router-v2/hooks.native.tsx index 6e51de3c3e3e..4be4928cb44a 100644 --- a/shared/router-v2/hooks.native.tsx +++ b/shared/router-v2/hooks.native.tsx @@ -97,8 +97,7 @@ export const useInitialState = (loggedInLoaded: boolean) => { } setInitialStateState('loading') const loadInitialURL = async () => { - const timeout = new Promise(resolve => setTimeout(() => resolve(null), 100)) - let url = await Promise.race([Linking.getInitialURL(), timeout]) + let url = await Linking.getInitialURL() // don't try and resume or follow links if we're signed out if (!loggedIn) { From 68215fbedd7a84492e9d67c8e844306cb06c3e22 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:03:02 -0400 Subject: [PATCH 18/42] main thread watchdog, flush buffered writer (#29042) --- go/bind/keybase.go | 29 ++- go/logger/buffer.go | 16 +- go/logger/file.go | 13 ++ go/logger/global.go | 1 + shared/ios/Keybase.xcodeproj/project.pbxproj | 4 + shared/ios/Keybase/AppDelegate.swift | 31 +++- shared/ios/Keybase/MainThreadWatchdog.swift | 181 +++++++++++++++++++ 7 files changed, 254 insertions(+), 21 deletions(-) create mode 100644 shared/ios/Keybase/MainThreadWatchdog.swift diff --git a/go/bind/keybase.go b/go/bind/keybase.go index 117bf2768bc6..46a598215680 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -591,6 +591,12 @@ func IsAppStateForeground() bool { return kbCtx.MobileAppState.State() == keybase1.MobileAppState_FOREGROUND } +// FlushLogs synchronously flushes any buffered log data to disk. Call this +// before background suspension and after foreground resume to prevent log loss. +func FlushLogs() { + logger.FlushLogFile() +} + func SetAppStateForeground() { if !isInited() { return @@ -640,36 +646,39 @@ func waitForInit(maxDur time.Duration) error { } } -func BackgroundSync() { +// BackgroundSync runs a short background sync pulse. Returns a non-empty status +// string on early exit or error so Swift can log it via NSLog. +func BackgroundSync() string { // On Android there is a race where this function can be called before Init when starting up in the // background. Let's wait a little bit here for Init to get run, and bail out if it never does. if err := waitForInit(5 * time.Second); err != nil { - return + return fmt.Sprintf("waitForInit timeout: %v", err) } defer kbCtx.Trace("BackgroundSync", nil)() // Skip the sync if we aren't in the background if state := kbCtx.MobileAppState.State(); state != keybase1.MobileAppState_BACKGROUND { - kbCtx.Log.Debug("BackgroundSync: skipping, app not in background state: %v", state) - return + msg := fmt.Sprintf("skipping, app not in background state: %v", state) + kbCtx.Log.Debug("BackgroundSync: %s", msg) + return msg } nextState := keybase1.MobileAppState_BACKGROUNDACTIVE kbCtx.MobileAppState.Update(nextState) - doneCh := make(chan struct{}) + resultCh := make(chan string, 1) go func() { - defer func() { close(doneCh) }() select { case state := <-kbCtx.MobileAppState.NextUpdate(&nextState): // if literally anything happens, let's get out of here - kbCtx.Log.Debug("BackgroundSync: bailing out early, appstate change: %v", state) - return + msg := fmt.Sprintf("bailing out early, appstate change: %v", state) + kbCtx.Log.Debug("BackgroundSync: %s", msg) + resultCh <- msg case <-time.After(10 * time.Second): kbCtx.MobileAppState.Update(keybase1.MobileAppState_BACKGROUND) - return + resultCh <- "completed 10s window" } }() - <-doneCh + return <-resultCh } // pushPendingMessageFailure sends at most one notification that a message diff --git a/go/logger/buffer.go b/go/logger/buffer.go index 819bd702837e..91f6cab9e14b 100644 --- a/go/logger/buffer.go +++ b/go/logger/buffer.go @@ -81,12 +81,7 @@ func (writer *autoFlushingBufferedWriter) backgroundFlush() { select { case <-writer.timer.C: // Swap out active and backup writers - writer.lock.Lock() - writer.bufferedWriter, writer.backupWriter = writer. - backupWriter, writer.bufferedWriter - writer.lock.Unlock() - - writer.backupWriter.Flush() + writer.Flush() case <-writer.shutdown: writer.timer.shutdownCh <- struct{}{} writer.bufferedWriter.Flush() @@ -127,6 +122,15 @@ func NewAutoFlushingBufferedWriter(baseWriter io.Writer, return result, result.shutdown, result.doneShutdown } +// Flush synchronously flushes any buffered data to the underlying writer. +// Safe to call concurrently with Write. +func (writer *autoFlushingBufferedWriter) Flush() { + writer.lock.Lock() + writer.bufferedWriter, writer.backupWriter = writer.backupWriter, writer.bufferedWriter + writer.lock.Unlock() + _ = writer.backupWriter.Flush() +} + func (writer *autoFlushingBufferedWriter) Write(p []byte) (int, error) { // The locked resource here, the pointer bufferedWriter, is only being read // even though this function is Write. diff --git a/go/logger/file.go b/go/logger/file.go index e59f796d1028..4ec2f4af44fe 100644 --- a/go/logger/file.go +++ b/go/logger/file.go @@ -62,6 +62,7 @@ func SetLogFileConfig(lfc *LogFileConfig, blc *BufferedLoggerConfig) error { if first { buf, shutdown, _ := NewAutoFlushingBufferedWriter(w, blc) w.stopFlushing = shutdown + currentBufferedWriter = buf.(*autoFlushingBufferedWriter) fileBackend := logging.NewLogBackend(buf, "", 0) logging.SetBackend(fileBackend) @@ -105,6 +106,18 @@ func (lfw *LogFileWriter) Open(at time.Time) error { return nil } +// FlushLogFile synchronously flushes any buffered log data to disk. +// Call this before the app is suspended or after resuming from background +// to ensure logs are not lost if the process is killed. +func FlushLogFile() { + globalLock.Lock() + buf := currentBufferedWriter + globalLock.Unlock() + if buf != nil { + buf.Flush() + } +} + func (lfw *LogFileWriter) Close() error { if lfw == nil { return nil diff --git a/go/logger/global.go b/go/logger/global.go index fb8d947362dc..fb25add2f78b 100644 --- a/go/logger/global.go +++ b/go/logger/global.go @@ -12,6 +12,7 @@ var ( globalLock sync.Mutex stderrIsTerminal = isatty.IsTerminal(os.Stderr.Fd()) currentLogFileWriter *LogFileWriter + currentBufferedWriter *autoFlushingBufferedWriter stdErrLoggingShutdown chan<- struct{} stdErrLoggingShutdownDone <-chan struct{} ) diff --git a/shared/ios/Keybase.xcodeproj/project.pbxproj b/shared/ios/Keybase.xcodeproj/project.pbxproj index 13846bfe64fa..f36a7d56797f 100644 --- a/shared/ios/Keybase.xcodeproj/project.pbxproj +++ b/shared/ios/Keybase.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ DB33CB6B2DFB02E9000472AA /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33CB6A2DFB02E9000472AA /* ShareViewController.swift */; }; DB59F9B72238A27E00E271C1 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB59F97B2238A27E00E271C1 /* JavaScriptCore.framework */; }; DBB8CC212DF336C200D43215 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8CC202DF336C200D43215 /* AppDelegate.swift */; }; + DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */; }; DBBE59452DF394F700A74A2D /* keybasego.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBBE59442DF394F700A74A2D /* keybasego.xcframework */; }; DBD252982DF32D5C008A43FF /* Fs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD252972DF32D5C008A43FF /* Fs.swift */; }; DBDCF30E1B8D03DD00BA95D8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */; }; @@ -111,6 +112,7 @@ DB33CB6A2DFB02E9000472AA /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DB59F97B2238A27E00E271C1 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; DBB8CC202DF336C200D43215 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadWatchdog.swift; sourceTree = ""; }; DBBE59442DF394F700A74A2D /* keybasego.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = keybasego.xcframework; sourceTree = ""; }; DBD252972DF32D5C008A43FF /* Fs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fs.swift; sourceTree = ""; }; DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; @@ -268,6 +270,7 @@ children = ( 005BF9961BB9C6B000BD8953 /* Keybase.entitlements */, DBB8CC202DF336C200D43215 /* AppDelegate.swift */, + DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */, DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */, DBDCF3091B8D03DD00BA95D8 /* Info.plist */, DB07050422E21B8B002F273D /* KeepThisFile.swift */, @@ -584,6 +587,7 @@ buildActionMask = 2147483647; files = ( DBB8CC212DF336C200D43215 /* AppDelegate.swift in Sources */, + DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */, DB07050522E21B8B002F273D /* KeepThisFile.swift in Sources */, DBDF89F62DF7779900EA18C2 /* Pusher.swift in Sources */, DBF123462DF1234500A12345 /* ShareIntentDonatorImpl.swift in Sources */, diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 29fe3a34c356..ccb9569430a8 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -45,6 +45,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID var iph: ItemProviderHelper? var startupLogFileHandle: FileHandle? + private var watchdog: MainThreadWatchdog? + public override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil @@ -58,6 +60,12 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID FsPathsHolder.shared().fsPaths = self.fsPaths self.writeStartupTimingLog("didFinishLaunchingWithOptions start") + let wd = MainThreadWatchdog(appStartTime: AppDelegate.appStartTime, writeLog: { [weak self] msg in + self?.writeStartupTimingLog(msg) + }) + wd.install() + wd.start(context: "cold start") + self.watchdog = wd self.didLaunchSetupBefore() @@ -163,7 +171,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) let dateString = dateFormatter.string(from: now) let timestamp = String(format: "%@.%06dZ", dateString, microseconds) - + let fileName = (file as NSString).lastPathComponent let logMessage = String(format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message) @@ -210,7 +218,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let shareIntentDonator = ShareIntentDonatorImpl() Keybasego.KeybaseInit(self.fsPaths["homedir"], self.fsPaths["sharedHome"], self.fsPaths["logFile"], "prod", securityAccessGroupOverride, nil, nil, systemVer, isIPad, nil, isIOS, shareIntentDonator, &err) if let err { NSLog("KeybaseInit FAILED: \(err)") } - + self.writeStartupTimingLog("After Go init") } @@ -223,6 +231,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID case .inactive: Keybasego.KeybaseSetAppStateInactive() default: Keybasego.KeybaseSetAppStateForeground() } + NSLog("notifyAppState: done") + Keybasego.KeybaseFlushLogs() } func didLaunchSetupBefore() { @@ -280,11 +290,18 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - NSLog("Background fetch started...") + let fetchStart = CFAbsoluteTimeGetCurrent() + NSLog("Background fetch queued...") DispatchQueue.global(qos: .default).async { - Keybasego.KeybaseBackgroundSync() + NSLog("Background fetch started...") + let status = Keybasego.KeybaseBackgroundSync() + let elapsed = (CFAbsoluteTimeGetCurrent() - fetchStart) * 1000 + if status.isEmpty { + NSLog("Background fetch completed in %.0fms", elapsed) + } else { + NSLog("Background fetch completed in %.0fms: %@", elapsed, status) + } completionHandler(.newData) - NSLog("Background fetch completed...") } } @@ -374,6 +391,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationDidEnterBackground(_ application: UIApplication) { + watchdog?.start(context: "background entered") application.ignoreSnapshotOnNextApplicationLaunch() NSLog("applicationDidEnterBackground: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() @@ -383,6 +401,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID NSLog("applicationDidEnterBackground: notifying go.") let requestTime = Keybasego.KeybaseAppDidEnterBackground() NSLog("applicationDidEnterBackground: after notifying go.") + Keybasego.KeybaseFlushLogs() if requestTime && (self.shutdownTask == UIBackgroundTaskIdentifier.invalid) { let app = UIApplication.shared @@ -408,6 +427,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationDidBecomeActive(_ application: UIApplication) { + watchdog?.stop() + Keybasego.KeybaseFlushLogs() let elapsed = CFAbsoluteTimeGetCurrent() - AppDelegate.appStartTime writeStartupTimingLog(String(format: "applicationDidBecomeActive: %.1fms after launch", elapsed * 1000)) NSLog("applicationDidBecomeActive: hiding keyz screen.") diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift new file mode 100644 index 000000000000..0ee40b9e864b --- /dev/null +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -0,0 +1,181 @@ +import UIKit +import Darwin + +// File-scope globals required by the SIGUSR1 signal handler. +// Signal handlers cannot safely reference Swift objects, so these must be C-compatible globals. +private let kMaxStackFrames: Int32 = 128 +private var gMainStackFrames = [UnsafeMutableRawPointer?](repeating: nil, count: Int(kMaxStackFrames)) +private var gMainStackFrameCount: Int32 = 0 +private var gMainStackReady: Bool = false + +// Monitors the main thread for hangs by pinging it every second from a background thread. +// Captures a stack trace via SIGUSR1 on first hang detection. +// +// Lifecycle: +// 1. Call install() once on the main thread at app startup. +// 2. Call start(context:) when entering background (or at cold start). +// 3. Call stop() at the top of applicationDidBecomeActive. +class MainThreadWatchdog { + private var active = false + private let lock = NSLock() + private var lastPong: CFAbsoluteTime = 0 + private var bgEnterTime: CFAbsoluteTime = 0 + + private var mainThreadPthread: pthread_t? + private let appStartTime: CFAbsoluteTime + private let writeLog: (String) -> Void + + init(appStartTime: CFAbsoluteTime, writeLog: @escaping (String) -> Void) { + self.appStartTime = appStartTime + self.writeLog = writeLog + } + + // Must be called from the main thread. Captures pthread_self() and installs + // the SIGUSR1 handler that records the main thread stack on demand. + func install() { + mainThreadPthread = pthread_self() + signal(SIGUSR1) { _ in + gMainStackFrameCount = backtrace(&gMainStackFrames, kMaxStackFrames) + gMainStackReady = true + } + } + + func start(context: String) { + lock.lock() + if active { + lock.unlock() + return + } + active = true + let now = CFAbsoluteTimeGetCurrent() + lastPong = now + bgEnterTime = now + lock.unlock() + + writeLog("Watchdog: started (\(context))") + + DispatchQueue(label: "kb.startup.watchdog", qos: .utility).async { [weak self] in + self?.run() + } + } + + func stop() { + lock.lock() + let wasActive = active + active = false + lock.unlock() + if wasActive { + writeLog("Watchdog: stopped") + } + } + + // MARK: - Private + + private func run() { + var lastLogTime: CFAbsoluteTime = 0 + + while true { + Thread.sleep(forTimeInterval: 1.0) + + lock.lock() + let isActive = active + let lastPong = self.lastPong + let bgEnterTime = self.bgEnterTime + lock.unlock() + guard isActive else { break } + + let now = CFAbsoluteTimeGetCurrent() + let blockDuration = now - lastPong + + // If blockDuration is very large, the process was suspended by iOS (not a main thread hang). + // Reset the pong baseline and send one ping — the main thread should respond within a + // few seconds once unfrozen. If it doesn't, we'll start reporting a hang next iteration. + if blockDuration > 30.0 { + let bgElapsedSec = now - bgEnterTime + let msg = String(format: "Watchdog: process resumed after %.0fs suspension (%.0fs since background)", blockDuration, bgElapsedSec) + NSLog("[Startup] %@", msg) + lock.lock() + self.lastPong = now + lock.unlock() + lastLogTime = 0 + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.lock.lock() + self.lastPong = CFAbsoluteTimeGetCurrent() + self.lock.unlock() + } + continue + } + + let totalElapsedMs = (now - appStartTime) * 1000 + + if blockDuration >= 3.0 { + // Log at first detection, then every 5s to avoid spam + if lastLogTime == 0 || (now - lastLogTime) >= 5.0 { + let bgElapsedSec = now - bgEnterTime + let msg = String(format: "Watchdog: main thread blocked %.0fs after foreground resume (%.0fs since background, %.0fms since launch)", blockDuration, bgElapsedSec, totalElapsedMs) + NSLog("[Startup] %@", msg) + // Enqueue a write for when the main thread recovers + DispatchQueue.main.async { [weak self] in + self?.writeLog(msg) + } + // Capture main thread stack on first detection + if lastLogTime == 0 { + captureAndLogStackTrace() + } + lastLogTime = now + } + } else { + if lastLogTime != 0 { + let bgElapsedSec = now - bgEnterTime + let msg = String(format: "Watchdog: main thread unblocked (%.0fs since background, %.0fms since launch)", bgElapsedSec, totalElapsedMs) + NSLog("[Startup] %@", msg) + DispatchQueue.main.async { [weak self] in + self?.writeLog(msg) + } + lastLogTime = 0 + } + } + + // Ping: ask main thread to update the pong time + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.lock.lock() + self.lastPong = CFAbsoluteTimeGetCurrent() + self.lock.unlock() + } + } + } + + // Send SIGUSR1 to the main thread, wait briefly for the handler to run, then log the stack. + private func captureAndLogStackTrace() { + gMainStackReady = false + guard let tid = mainThreadPthread else { + NSLog("[Startup] Watchdog: main thread pthread not captured") + return + } + pthread_kill(tid, SIGUSR1) + // Spin up to 200ms for the signal handler to complete + for _ in 0..<20 { + if gMainStackReady { break } + Thread.sleep(forTimeInterval: 0.01) + } + guard gMainStackReady else { + NSLog("[Startup] Watchdog: stack capture timed out") + return + } + let count = Int(gMainStackFrameCount) + // Log the binary load slide so addresses can be symbolicated offline: + // atos -o Keybase.app.dSYM/Contents/Resources/DWARF/Keybase -l
+ let slide = _dyld_get_image_vmaddr_slide(0) + NSLog("[Startup] Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide) + gMainStackFrames.withUnsafeMutableBufferPointer { buf in + if let syms = backtrace_symbols(buf.baseAddress, Int32(count)) { + for i in 0.. Date: Mon, 23 Mar 2026 12:43:34 -0400 Subject: [PATCH 19/42] signal handler (#29045) --- shared/ios/Keybase/AppDelegate.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index ccb9569430a8..567469766a95 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -63,11 +63,14 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let wd = MainThreadWatchdog(appStartTime: AppDelegate.appStartTime, writeLog: { [weak self] msg in self?.writeStartupTimingLog(msg) }) - wd.install() wd.start(context: "cold start") self.watchdog = wd self.didLaunchSetupBefore() + // Install the SIGUSR1 stack-capture handler after KeybaseInit — Go's runtime + // initializes its own signal handlers during KeybaseInit and would overwrite + // anything registered before it. + wd.install() if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { let notificationDict = Dictionary(uniqueKeysWithValues: remoteNotification.map { (String(describing: $0.key), $0.value) }) @@ -464,8 +467,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationWillEnterForeground(_ application: UIApplication) { - NSLog("applicationWillEnterForeground: hiding keyz screen.") + NSLog("applicationWillEnterForeground: start, hiding keyz screen.") hideCover() + NSLog("applicationWillEnterForeground: done") } } From d453a98ffad81ee25088bbfe5d5df4d43f5bbeb4 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:13:12 -0400 Subject: [PATCH 20/42] fix crash: use throwing FileHandle APIs so do/catch guards against write failures (#29046) --- shared/ios/Keybase/AppDelegate.swift | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 567469766a95..d478c2d0b3fc 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -146,17 +146,14 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } if self.startupLogFileHandle == nil { - do { - if !FileManager.default.fileExists(atPath: logFilePath) { - FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) - } - - if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { - fileHandle.seekToEndOfFile() - self.startupLogFileHandle = fileHandle - } - } catch { - NSLog("Error opening startup timing log file: \(error)") + if !FileManager.default.fileExists(atPath: logFilePath) { + FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) + } + if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { + fileHandle.seekToEndOfFile() + self.startupLogFileHandle = fileHandle + } else { + NSLog("Error opening startup timing log file: \(logFilePath)") return } } @@ -183,8 +180,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } do { - fileHandle.write(logData) - fileHandle.synchronizeFile() + try fileHandle.write(contentsOf: logData) + try fileHandle.synchronize() } catch { NSLog("Error writing startup timing log: \(error)") } @@ -192,8 +189,12 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID private func closeStartupLogFile() { if let fileHandle = self.startupLogFileHandle { - fileHandle.synchronizeFile() - fileHandle.closeFile() + do { + try fileHandle.synchronize() + try fileHandle.close() + } catch { + NSLog("Error closing startup timing log: \(error)") + } self.startupLogFileHandle = nil } } From e9488ec2bac70777b76a2d188a905cc1fe649895 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:31:27 -0400 Subject: [PATCH 21/42] add advertisements to tuxbot (#29047) --- packaging/linux/tuxbot/bot/chatbot/chatbot.go | 4 +- packaging/linux/tuxbot/bot/common/acl.go | 6 +- packaging/linux/tuxbot/bot/tuxbot/tuxbot.go | 67 +++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/packaging/linux/tuxbot/bot/chatbot/chatbot.go b/packaging/linux/tuxbot/bot/chatbot/chatbot.go index 8081e305365b..51534ace3538 100644 --- a/packaging/linux/tuxbot/bot/chatbot/chatbot.go +++ b/packaging/linux/tuxbot/bot/chatbot/chatbot.go @@ -121,13 +121,13 @@ func (l ChatLogger) VDebug(format string, args ...interface{}) { func (l ChatLogger) Debug(format string, args ...interface{}) { msg := l.msg(format, args...) fmt.Println(msg) - if _, err := l.API.SendMessage(l.DebugChannel, msg); err != nil { + if _, err := l.API.SendMessage(l.DebugChannel, "%s", msg); err != nil { fmt.Printf("unable to SendMessage: %v", err) } } func (l ChatLogger) Info(format string, args ...interface{}) { - if _, err := l.API.SendMessage(l.InfoChannel, l.msg(format, args...)); err != nil { + if _, err := l.API.SendMessage(l.InfoChannel, "%s", l.msg(format, args...)); err != nil { fmt.Printf("unable to SendMessage: %v", err) } if l.InfoChannel.Name != l.DebugChannel.Name || l.InfoChannel.TopicName != l.DebugChannel.TopicName { diff --git a/packaging/linux/tuxbot/bot/common/acl.go b/packaging/linux/tuxbot/bot/common/acl.go index 3e7e72ae4bec..81902dfe0d2a 100644 --- a/packaging/linux/tuxbot/bot/common/acl.go +++ b/packaging/linux/tuxbot/bot/common/acl.go @@ -7,8 +7,10 @@ import ( var self access.Username = "tuxbot" -var tuxbotAdmins = []access.Username{self, "max", "mikem", "modalduality", "cjb", "jzila", - "patrick", "songgao", "strib", "joshblum", "mlsteele"} +var tuxbotAdmins = []access.Username{ + self, "mikem", "modalduality", + "patrick", "songgao", "joshblum", +} func SimpleTuxbotACL(infoChannel chat1.ChatChannel) access.ACL { return access.NewConstantACL(map[chat1.ChatChannel][]access.Username{ diff --git a/packaging/linux/tuxbot/bot/tuxbot/tuxbot.go b/packaging/linux/tuxbot/bot/tuxbot/tuxbot.go index d5a3c4b0dc18..1ca3e62df1dd 100644 --- a/packaging/linux/tuxbot/bot/tuxbot/tuxbot.go +++ b/packaging/linux/tuxbot/bot/tuxbot/tuxbot.go @@ -113,12 +113,12 @@ func (c Tuxbot) Dispatch(msg chat1.MsgSummary, args []string) (err error) { xzCmd := exec.Command("xz") archiveName := fmt.Sprintf("/keybase/team/%s/keybase-%s.tar.xz", c.archiveTeam, revision) - archiveHandle, err := os.OpenFile(archiveName, os.O_RDWR|os.O_CREATE, 0644) + archiveHandle, err := os.OpenFile(archiveName, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return err } defer archiveHandle.Close() - sigHandle, err := os.OpenFile(archiveName+".sig", os.O_RDWR|os.O_CREATE, 0644) + sigHandle, err := os.OpenFile(archiveName+".sig", os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return err } @@ -228,7 +228,7 @@ func (c Tuxbot) Dispatch(msg chat1.MsgSummary, args []string) (err error) { return } - if _, err := c.API().SendMessage(c.sendChannel, "!build-docker "+strings.Join(args, " ")); err != nil { + if _, err := c.API().SendMessage(c.sendChannel, "%s", "!build-docker "+strings.Join(args, " ")); err != nil { c.Debug("unable to SendMessage: %v", err) } }() @@ -444,7 +444,7 @@ func (c Tuxbot) Dispatch(msg chat1.MsgSummary, args []string) (err error) { } func execToFile(filename string, cmd *exec.Cmd) error { - handle, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) + handle, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return err } @@ -463,6 +463,61 @@ func execToFile(filename string, cmd *exec.Cmd) error { return nil } +func (c Tuxbot) advertisement() kbchat.Advertisement { + commands := []chat1.UserBotCommandInput{ + { + Name: "release", + Description: "Build Linux release packages from HEAD or a specified revision.", + }, + { + Name: "nightly", + Description: "Build nightly Linux packages from HEAD or a specified revision.", + }, + { + Name: "test", + Description: "Build Linux test packages from HEAD or a specified revision.", + }, + { + Name: "archive", + Description: "Create and sign Linux source release artifacts for a revision.", + }, + { + Name: "build-docker", + Description: "Build and push Docker images for HEAD or a specified revision.", + }, + { + Name: "release-docker", + Description: "Promote an existing Docker tag as a release.", + }, + { + Name: "tuxjournal", + Description: "Write the recent tuxbot journal to the archive team folder.", + }, + { + Name: "journal", + Description: "Write the recent system journal to the archive team folder.", + }, + { + Name: "cleanup", + Description: "Run the local cleanup helper on the tuxbot host.", + }, + { + Name: "restartdocker", + Description: "Restart Docker on the tuxbot host.", + }, + } + + return kbchat.Advertisement{ + Alias: c.Name, + Advertisements: []chat1.AdvertiseCommandAPIParam{ + { + Typ: "public", + Commands: commands, + }, + }, + } +} + func main() { err := gotenv.Load(fmt.Sprintf("/keybase/team/%s/.kbfs_autogit/%s/tuxbot.env", os.Getenv("SECRETS_TEAM"), os.Getenv("SECRETS_REPO"))) if err != nil { @@ -510,6 +565,10 @@ func main() { archiveTeam: *infoTeam, } + if _, err := tuxbot.API().AdvertiseCommands(tuxbot.advertisement()); err != nil { + tuxbot.Info("unable to advertise commands: %v", err) + } + if err := chatbot.Listen(tuxbot); err != nil { panic(err) } From b5a7bc2f6c4c863fb41b40d3e3508e2ccbb841d7 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 23 Mar 2026 15:09:17 -0400 Subject: [PATCH 22/42] edge to edge fix (#29048) --- .../java/io/keybase/ossifrage/MainActivity.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt index 5099014b8a57..aa2146487764 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt @@ -92,18 +92,19 @@ class MainActivity : ReactActivity() { scheduleHandleIntent() - // fix for keyboard avoiding not working on 35 - if (Build.VERSION.SDK_INT >= 35) { - val rootView = findViewById(android.R.id.content) - ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> - val innerPadding = insets.getInsets(WindowInsetsCompat.Type.ime()) + // edgeToEdgeEnabled=true in gradle.properties causes RN to call + // WindowCompat.setDecorFitsSystemWindows(false) on all API levels, which breaks + // adjustResize. Manually apply insets so the keyboard pushes content up. + val rootView = findViewById(android.R.id.content) + ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val sysBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) rootView.setPadding( - innerPadding.left, - innerPadding.top, - innerPadding.right, - innerPadding.bottom) + sysBarInsets.left, + sysBarInsets.top, + sysBarInsets.right, + maxOf(imeInsets.bottom, sysBarInsets.bottom)) insets - } } } From f003df20468f332543be4074470a89bfec359305 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Mon, 23 Mar 2026 16:12:43 -0400 Subject: [PATCH 23/42] update 6.6.2 (#29051) --- go/libkb/version.go | 2 +- shared/android/app/build.gradle | 2 +- shared/ios/Keybase/Info.plist | 2 +- shared/ios/KeybaseShare/Info.plist | 2 +- skills/bumping-versions/SKILL.md | 15 +++++++++++++++ 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 skills/bumping-versions/SKILL.md diff --git a/go/libkb/version.go b/go/libkb/version.go index 9d57a10e1134..4740e6d87f04 100644 --- a/go/libkb/version.go +++ b/go/libkb/version.go @@ -4,4 +4,4 @@ package libkb // Version is the current version (should be MAJOR.MINOR.PATCH) -const Version = "6.6.1" +const Version = "6.6.2" diff --git a/shared/android/app/build.gradle b/shared/android/app/build.gradle index cdc3d05e206d..b20b867dd6fd 100644 --- a/shared/android/app/build.gradle +++ b/shared/android/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: "com.facebook.react" apply plugin: 'com.github.triplet.play' // KB: app version -def VERSION_NAME = "6.6.1" +def VERSION_NAME = "6.6.2" // KB: Number of commits, like ios Integer getVersionCode() { diff --git a/shared/ios/Keybase/Info.plist b/shared/ios/Keybase/Info.plist index 9d97bbe2ad2a..162c8786e7e6 100644 --- a/shared/ios/Keybase/Info.plist +++ b/shared/ios/Keybase/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 6.6.1 + 6.6.2 CFBundleSignature ???? CFBundleURLTypes diff --git a/shared/ios/KeybaseShare/Info.plist b/shared/ios/KeybaseShare/Info.plist index 52eba07f328d..6fe231622ccb 100644 --- a/shared/ios/KeybaseShare/Info.plist +++ b/shared/ios/KeybaseShare/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 6.6.1 + 6.6.2 CFBundleVersion 200 NSExtension diff --git a/skills/bumping-versions/SKILL.md b/skills/bumping-versions/SKILL.md new file mode 100644 index 000000000000..1497ded65195 --- /dev/null +++ b/skills/bumping-versions/SKILL.md @@ -0,0 +1,15 @@ +--- +name: bumping-versions +description: Use when upgrading the Keybase client version number - lists all files that must be updated together +--- + +# Bumping Versions + +Update all four files together when changing the version number: + +| File | Field | +|------|-------| +| `go/libkb/version.go` | `const Version = "X.X.X"` | +| `shared/ios/Keybase/Info.plist` | `CFBundleShortVersionString` | +| `shared/ios/KeybaseShare/Info.plist` | `CFBundleShortVersionString` | +| `shared/android/app/build.gradle` | `def VERSION_NAME = "X.X.X"` | From fb189fd7bdff94d6ec508852abcf744efc75a777 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:08:30 -0400 Subject: [PATCH 24/42] more startup debugging (#29050) --- .../ossifrage/modules/BackgroundSyncWorker.kt | 4 +-- shared/ios/Keybase/MainThreadWatchdog.swift | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/BackgroundSyncWorker.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/BackgroundSyncWorker.kt index f80fd27fcdf3..93632cde97a2 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/BackgroundSyncWorker.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/BackgroundSyncWorker.kt @@ -11,8 +11,8 @@ class BackgroundSyncWorker( params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { Log.d(TAG, "Background sync start.") - Keybase.backgroundSync() - Log.d(TAG, "Background sync complete.") + val result = Keybase.backgroundSync() + Log.d(TAG, "Background sync complete: $result") return Result.success() } diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index 0ee40b9e864b..7bceb1af59d0 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -20,6 +20,7 @@ class MainThreadWatchdog { private let lock = NSLock() private var lastPong: CFAbsoluteTime = 0 private var bgEnterTime: CFAbsoluteTime = 0 + private var isBackgroundContext = false private var mainThreadPthread: pthread_t? private let appStartTime: CFAbsoluteTime @@ -50,6 +51,7 @@ class MainThreadWatchdog { let now = CFAbsoluteTimeGetCurrent() lastPong = now bgEnterTime = now + isBackgroundContext = (context == "background entered") lock.unlock() writeLog("Watchdog: started (\(context))") @@ -81,16 +83,20 @@ class MainThreadWatchdog { let isActive = active let lastPong = self.lastPong let bgEnterTime = self.bgEnterTime + let isBackgroundContext = self.isBackgroundContext lock.unlock() guard isActive else { break } let now = CFAbsoluteTimeGetCurrent() let blockDuration = now - lastPong - // If blockDuration is very large, the process was suspended by iOS (not a main thread hang). - // Reset the pong baseline and send one ping — the main thread should respond within a - // few seconds once unfrozen. If it doesn't, we'll start reporting a hang next iteration. - if blockDuration > 30.0 { + // If the process was suspended by iOS, blockDuration reflects the suspension gap rather + // than a real main-thread hang. Two cases: + // 1. Background-entered watchdog: ANY gap >= 3s is a suspension — iOS suspends apps + // aggressively even after a few seconds in the background, so the 30s threshold + // below is too coarse and produces false positives for 5–29s suspensions. + // 2. Cold-start or foreground watchdog: use the 30s threshold as before. + if blockDuration > 30.0 || (isBackgroundContext && blockDuration >= 3.0) { let bgElapsedSec = now - bgEnterTime let msg = String(format: "Watchdog: process resumed after %.0fs suspension (%.0fs since background)", blockDuration, bgElapsedSec) NSLog("[Startup] %@", msg) @@ -110,8 +116,9 @@ class MainThreadWatchdog { let totalElapsedMs = (now - appStartTime) * 1000 if blockDuration >= 3.0 { - // Log at first detection, then every 5s to avoid spam - if lastLogTime == 0 || (now - lastLogTime) >= 5.0 { + // Sample every 2s for the duration of the hang so we capture how the main thread + // evolves (e.g. keychain IPC → rendering → idle) rather than a single snapshot. + if lastLogTime == 0 || (now - lastLogTime) >= 2.0 { let bgElapsedSec = now - bgEnterTime let msg = String(format: "Watchdog: main thread blocked %.0fs after foreground resume (%.0fs since background, %.0fms since launch)", blockDuration, bgElapsedSec, totalElapsedMs) NSLog("[Startup] %@", msg) @@ -119,10 +126,8 @@ class MainThreadWatchdog { DispatchQueue.main.async { [weak self] in self?.writeLog(msg) } - // Capture main thread stack on first detection - if lastLogTime == 0 { - captureAndLogStackTrace() - } + // Capture a stack trace on every sample interval, not just the first. + captureAndLogStackTrace() lastLogTime = now } } else { From 4018d6165e529ef517e4a022395480b76c06f954 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:47:05 -0400 Subject: [PATCH 25/42] Modernize swift code (#29054) --- shared/ios/Keybase/AppDelegate.swift | 25 +++++++------- shared/ios/Keybase/Fs.swift | 50 +++++++++++++++++----------- shared/ios/Keybase/Pusher.swift | 13 ++++---- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index d478c2d0b3fc..463d6f648bb6 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -140,6 +140,13 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID private static var appStartTime: CFAbsoluteTime = 0 + private static let logDateFormatter: DateFormatter = { + let f = DateFormatter() + f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + f.timeZone = TimeZone(secondsFromGMT: 0) + return f + }() + private func writeStartupTimingLog(_ message: String, file: String = #file, line: Int = #line) { guard let logFilePath = self.fsPaths["logFile"], !logFilePath.isEmpty else { return @@ -164,15 +171,11 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let now = Date() let timeInterval = now.timeIntervalSince1970 - let seconds = Int(timeInterval) - let microseconds = Int((timeInterval - Double(seconds)) * 1_000_000) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - let dateString = dateFormatter.string(from: now) + let microseconds = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1_000_000) + let dateString = AppDelegate.logDateFormatter.string(from: now) let timestamp = String(format: "%@.%06dZ", dateString, microseconds) - let fileName = (file as NSString).lastPathComponent + let fileName = URL(fileURLWithPath: file).lastPathComponent let logMessage = String(format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message) guard let logData = logMessage.data(using: .utf8) else { @@ -261,7 +264,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.resignImageView?.alpha = 0 self.resignImageView?.backgroundColor = rootView.backgroundColor self.resignImageView?.image = UIImage(named: "LaunchImage") - self.window?.addSubview(self.resignImageView!) + if let view = self.resignImageView { self.window?.addSubview(view) } UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) } @@ -384,7 +387,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID public override func applicationWillResignActive(_ application: UIApplication) { NSLog("applicationWillResignActive: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() - self.resignImageView?.superview?.bringSubviewToFront(self.resignImageView!) + if let view = self.resignImageView { view.superview?.bringSubviewToFront(view) } NSLog("applicationWillResignActive: rendering keyz screen...") UIView.animate(withDuration: 0.3, delay: 0.1, options: .beginFromCurrentState) { self.resignImageView?.alpha = 1 @@ -444,8 +447,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID // This handles the case where app was backgrounded and notification was clicked // but React Native wasn't ready yet if let storedNotification = KbGetAndClearInitialNotification() { - let type = storedNotification["type"] as? String ?? "unknown" - let convID = storedNotification["convID"] as? String ?? storedNotification["c"] as? String ?? "unknown" let userInteraction = storedNotification["userInteraction"] as? Bool ?? false if userInteraction { @@ -457,7 +458,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID KbEmitPushNotification(storedNotification) var copy = Dictionary(uniqueKeysWithValues: storedNotification.map { (String(describing: $0.key), $0.value) }) copy["reEmittedInBecomeActive"] = true - KbSetInitialNotification(copy as NSDictionary as! [AnyHashable : Any]) + KbSetInitialNotification(copy) } } else { NSLog("applicationDidBecomeActive: stored notification has userInteraction=false, skipping") diff --git a/shared/ios/Keybase/Fs.swift b/shared/ios/Keybase/Fs.swift index 10dc2b486a99..7b6463e49ace 100644 --- a/shared/ios/Keybase/Fs.swift +++ b/shared/ios/Keybase/Fs.swift @@ -16,19 +16,22 @@ import Foundation let appKeybasePath = Self.getAppKeybasePath() // Put logs in a subdir that is entirely background readable - let oldLogPath = ("~/Library/Caches/Keybase" as NSString).expandingTildeInPath - let logPath = (oldLogPath as NSString).appendingPathComponent("logs") - let serviceLogFile = skipLogFile ? "" : (logPath as NSString).appendingPathComponent("ios.log") + let oldLogURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + .appendingPathComponent("Keybase") + let serviceLogFile = skipLogFile ? "" : oldLogURL + .appendingPathComponent("logs") + .appendingPathComponent("ios.log").path if !skipLogFile { // cleanup old log files let fm = FileManager.default ["ios.log", "ios.log.ek"].forEach { - try? fm.removeItem(atPath: (oldLogPath as NSString).appendingPathComponent($0)) + try? fm.removeItem(at: oldLogURL.appendingPathComponent($0)) } } // Create LevelDB and log directories with a slightly lower data protection // mode so we can use them in the background + let appKeybaseURL = URL(fileURLWithPath: appKeybasePath) [ "keybase.chat.leveldb", "keybase.leveldb", @@ -44,10 +47,10 @@ import Foundation "synced_tlf_config", "logs" ].forEach { - createBackgroundReadableDirectory(path: (appKeybasePath as NSString).appendingPathComponent($0), setAllFiles: true) + createBackgroundReadableDirectory(path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) } // Mark avatars, which are in the caches dir - createBackgroundReadableDirectory(path: (oldLogPath as NSString).appendingPathComponent("avatars"), setAllFiles: true) + createBackgroundReadableDirectory(path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) let setupFsElapsed = CFAbsoluteTimeGetCurrent() - setupFsStartTime NSLog("setupFs: completed in %.3f seconds", setupFsElapsed) @@ -60,9 +63,11 @@ import Foundation } private func addSkipBackupAttribute(to path: String) -> Bool { - let url = Foundation.URL(fileURLWithPath: path) + var url = URL(fileURLWithPath: path) do { - try (url as NSURL).setResourceValue(true, forKey: URLResourceKey.isExcludedFromBackupKey) + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try url.setResourceValues(resourceValues) return true } catch { NSLog("Error excluding \(url.lastPathComponent) from backup \(error)") @@ -93,10 +98,11 @@ import Foundation NSLog("setAllFiles is true charging forward") // Recursively set attributes on all subdirectories and files + let baseURL = URL(fileURLWithPath: path) var fileCount = 0 if let enumerator = fm.enumerator(atPath: path) { for case let file as String in enumerator { - let filePath = (path as NSString).appendingPathComponent(file) + let filePath = baseURL.appendingPathComponent(file).path do { try fm.setAttributes(noProt, ofItemAtPath: filePath) fileCount += 1 @@ -113,12 +119,14 @@ import Foundation private func maybeMigrateDirectory(source: String, dest: String) -> Bool { let fm = FileManager.default + let sourceURL = URL(fileURLWithPath: source) + let destURL = URL(fileURLWithPath: dest) do { - // Always do this move in case it doesn't work on previous attempts. + // Always do this move in case it doesn't work on previous attempts. let sourceContents = try fm.contentsOfDirectory(atPath: source) for file in sourceContents { - let path = (source as NSString).appendingPathComponent(file) - let destPath = (dest as NSString).appendingPathComponent(file) + let path = sourceURL.appendingPathComponent(file).path + let destPath = destURL.appendingPathComponent(file).path var isDir: ObjCBool = false if fm.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue { NSLog("skipping directory: \(file)") @@ -142,18 +150,20 @@ import Foundation } @objc static func getAppKeybasePath() -> String { - return ("~/Library/Application Support/Keybase" as NSString).expandingTildeInPath + return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("Keybase").path } @objc static func getEraseableKVPath() -> String { - return (getAppKeybasePath() as NSString).appendingPathComponent("eraseablekvstore/device-eks") + return URL(fileURLWithPath: getAppKeybasePath()) + .appendingPathComponent("eraseablekvstore/device-eks").path } private func setupAppHome(home: String, sharedHome: String) -> String { let tempUrl = FileManager.default.temporaryDirectory - // workaround a problem where iOS dyld3 loader crashes if accessing .closure files - // with complete data protection on - let dyldDir = (tempUrl.path as NSString).appendingPathComponent("com.apple.dyld") + // workaround a problem where iOS dyld3 loader crashes if accessing .closure files + // with complete data protection on + let dyldDir = tempUrl.appendingPathComponent("com.apple.dyld").path let appKeybasePath = Self.getAppKeybasePath() let appEraseableKVPath = Self.getEraseableKVPath() @@ -168,8 +178,10 @@ import Foundation private func setupSharedHome(home: String, sharedHome: String) -> String { let appKeybasePath = Self.getAppKeybasePath() let appEraseableKVPath = Self.getEraseableKVPath() - let sharedKeybasePath = (sharedHome as NSString).appendingPathComponent("Library/Application Support/Keybase") - let sharedEraseableKVPath = (sharedKeybasePath as NSString).appendingPathComponent("eraseablekvstore/device-eks") + let sharedKeybasePath = URL(fileURLWithPath: sharedHome) + .appendingPathComponent("Library/Application Support/Keybase").path + let sharedEraseableKVPath = URL(fileURLWithPath: sharedKeybasePath) + .appendingPathComponent("eraseablekvstore/device-eks").path createBackgroundReadableDirectory(path: sharedKeybasePath, setAllFiles: true) createBackgroundReadableDirectory(path: sharedEraseableKVPath, setAllFiles: true) diff --git a/shared/ios/Keybase/Pusher.swift b/shared/ios/Keybase/Pusher.swift index a32c59669693..75650447a745 100644 --- a/shared/ios/Keybase/Pusher.swift +++ b/shared/ios/Keybase/Pusher.swift @@ -3,7 +3,7 @@ import UserNotifications import Keybasego class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { - func localNotification(_ ident: String?, msg: String?, badgeCount: Int, soundName: String?, convID: String?, typ: String?) { + func localNotification(_ ident: String?, msg: String?, badgeCount: Int, soundName: String?, convID: String?, typ: String?) { let content = UNMutableNotificationContent() if let soundName = soundName { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName)) @@ -18,19 +18,18 @@ class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { } } } - + func display(_ n: KeybaseChatNotification?) { - guard let notification = n else { return } - guard let message = notification.message else { return } - + guard let notification = n, let message = notification.message else { return } + let ident = "\(notification.convID):\(message.id_)" let msg: String if notification.isPlaintext && !message.plaintext.isEmpty { let username = message.from?.keybaseUsername ?? "" let convName = notification.conversationName msg = (username == convName || convName.isEmpty) - ? "\(username): \(message.plaintext)" - : "\(username) (\(convName)): \(message.plaintext)" + ? "\(username): \(message.plaintext)" + : "\(username) (\(convName)): \(message.plaintext)" } else { msg = message.serverMessage } From b4fd64687da0108ce3b55ec5eee10200cffc0465 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:10:24 -0400 Subject: [PATCH 26/42] startup debug logging (#29055) --- shared/ios/Keybase/MainThreadWatchdog.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index 7bceb1af59d0..23cc9072f8ec 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -153,10 +153,12 @@ class MainThreadWatchdog { } // Send SIGUSR1 to the main thread, wait briefly for the handler to run, then log the stack. + // Frames are collected synchronously here on the watchdog thread, then dispatched to the main + // thread via writeLog so they appear in ios.log (captured by logsend). private func captureAndLogStackTrace() { gMainStackReady = false guard let tid = mainThreadPthread else { - NSLog("[Startup] Watchdog: main thread pthread not captured") + DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: main thread pthread not captured") } return } pthread_kill(tid, SIGUSR1) @@ -166,21 +168,28 @@ class MainThreadWatchdog { Thread.sleep(forTimeInterval: 0.01) } guard gMainStackReady else { - NSLog("[Startup] Watchdog: stack capture timed out") + DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: stack capture timed out") } return } let count = Int(gMainStackFrameCount) - // Log the binary load slide so addresses can be symbolicated offline: + // Collect the binary load slide so addresses can be symbolicated offline: // atos -o Keybase.app.dSYM/Contents/Resources/DWARF/Keybase -l
let slide = _dyld_get_image_vmaddr_slide(0) - NSLog("[Startup] Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide) + // Build the frame strings synchronously while the globals are still valid, then + // dispatch a single block to write them all once the main thread unblocks. + var lines = [String]() + lines.append(String(format: "Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide)) gMainStackFrames.withUnsafeMutableBufferPointer { buf in if let syms = backtrace_symbols(buf.baseAddress, Int32(count)) { for i in 0.. Date: Wed, 25 Mar 2026 13:21:48 -0400 Subject: [PATCH 27/42] increase watchdog frequency (#29067) --- shared/ios/Keybase.xcodeproj/project.pbxproj | 4 ++-- shared/ios/Keybase/MainThreadWatchdog.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shared/ios/Keybase.xcodeproj/project.pbxproj b/shared/ios/Keybase.xcodeproj/project.pbxproj index f36a7d56797f..ee9d8d2255fb 100644 --- a/shared/ios/Keybase.xcodeproj/project.pbxproj +++ b/shared/ios/Keybase.xcodeproj/project.pbxproj @@ -36,12 +36,12 @@ DB33CB6B2DFB02E9000472AA /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33CB6A2DFB02E9000472AA /* ShareViewController.swift */; }; DB59F9B72238A27E00E271C1 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB59F97B2238A27E00E271C1 /* JavaScriptCore.framework */; }; DBB8CC212DF336C200D43215 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8CC202DF336C200D43215 /* AppDelegate.swift */; }; - DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */; }; DBBE59452DF394F700A74A2D /* keybasego.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBBE59442DF394F700A74A2D /* keybasego.xcframework */; }; DBD252982DF32D5C008A43FF /* Fs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD252972DF32D5C008A43FF /* Fs.swift */; }; DBDCF30E1B8D03DD00BA95D8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */; }; DBDF89F62DF7779900EA18C2 /* Pusher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDF89F52DF7779900EA18C2 /* Pusher.swift */; }; DBF123462DF1234500A12345 /* ShareIntentDonatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF123452DF1234500A12345 /* ShareIntentDonatorImpl.swift */; }; + DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -112,7 +112,6 @@ DB33CB6A2DFB02E9000472AA /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DB59F97B2238A27E00E271C1 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; DBB8CC202DF336C200D43215 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadWatchdog.swift; sourceTree = ""; }; DBBE59442DF394F700A74A2D /* keybasego.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = keybasego.xcframework; sourceTree = ""; }; DBD252972DF32D5C008A43FF /* Fs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fs.swift; sourceTree = ""; }; DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; @@ -120,6 +119,7 @@ DBDCF3441B8D04FC00BA95D8 /* Keybase-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Keybase-Bridging-Header.h"; sourceTree = ""; }; DBDF89F52DF7779900EA18C2 /* Pusher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pusher.swift; sourceTree = ""; }; DBF123452DF1234500A12345 /* ShareIntentDonatorImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareIntentDonatorImpl.swift; sourceTree = ""; }; + DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadWatchdog.swift; sourceTree = ""; }; F68DC40B579A1F9AC0F34950 /* Pods_KeybaseShare.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KeybaseShare.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index 23cc9072f8ec..d88a1f1c09c2 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -77,7 +77,7 @@ class MainThreadWatchdog { var lastLogTime: CFAbsoluteTime = 0 while true { - Thread.sleep(forTimeInterval: 1.0) + Thread.sleep(forTimeInterval: 0.5) lock.lock() let isActive = active @@ -115,12 +115,12 @@ class MainThreadWatchdog { let totalElapsedMs = (now - appStartTime) * 1000 - if blockDuration >= 3.0 { - // Sample every 2s for the duration of the hang so we capture how the main thread + if blockDuration >= 1.0 { + // Sample every 1s for the duration of the hang so we capture how the main thread // evolves (e.g. keychain IPC → rendering → idle) rather than a single snapshot. - if lastLogTime == 0 || (now - lastLogTime) >= 2.0 { + if lastLogTime == 0 || (now - lastLogTime) >= 1.0 { let bgElapsedSec = now - bgEnterTime - let msg = String(format: "Watchdog: main thread blocked %.0fs after foreground resume (%.0fs since background, %.0fms since launch)", blockDuration, bgElapsedSec, totalElapsedMs) + let msg = String(format: "Watchdog: main thread blocked %.1fs after foreground resume (%.0fs since background, %.0fms since launch)", blockDuration, bgElapsedSec, totalElapsedMs) NSLog("[Startup] %@", msg) // Enqueue a write for when the main thread recovers DispatchQueue.main.async { [weak self] in From ed7e7cc0dc7eaba9dfd616c3c907ff2be28e7bd0 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:39:49 -0400 Subject: [PATCH 28/42] fix fmt pod for new xcode (#29068) * fix fmt pod for new xcode * update fmt dep to 12.1.0 --- shared/ios/Podfile | 1 + shared/ios/Podfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/shared/ios/Podfile b/shared/ios/Podfile index 8dd9516bcbb4..73cd024161b1 100644 --- a/shared/ios/Podfile +++ b/shared/ios/Podfile @@ -32,6 +32,7 @@ target 'Keybase' do :mac_catalyst_enabled => false, # :ccache_enabled => true ) + end end diff --git a/shared/ios/Podfile.lock b/shared/ios/Podfile.lock index 6c16b2c5df90..53abe593feee 100644 --- a/shared/ios/Podfile.lock +++ b/shared/ios/Podfile.lock @@ -110,7 +110,7 @@ PODS: - UMAppLoader - fast_float (8.0.0) - FBLazyVector (0.81.5) - - fmt (11.0.2) + - fmt (12.1.0) - glog (0.3.5) - hermes-engine (0.81.5): - hermes-engine/Pre-built (= 0.81.5) @@ -167,20 +167,20 @@ PODS: - boost - DoubleConversion - fast_float (= 8.0.0) - - fmt (= 11.0.2) + - fmt (= 12.1.0) - glog - RCT-Folly/Default (= 2024.11.18.00) - RCT-Folly/Default (2024.11.18.00): - boost - DoubleConversion - fast_float (= 8.0.0) - - fmt (= 11.0.2) + - fmt (= 12.1.0) - glog - RCT-Folly/Fabric (2024.11.18.00): - boost - DoubleConversion - fast_float (= 8.0.0) - - fmt (= 11.0.2) + - fmt (= 12.1.0) - glog - RCTDeprecation (0.81.5) - RCTRequired (0.81.5) @@ -3342,7 +3342,7 @@ SPEC CHECKSUMS: EXTaskManager: 6f1a66e4c8cc6df6e24c3d90928704bc3013eae5 fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 5beb8028d5a2e75dd9634917f23e23d3a061d2aa - fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd + fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 KBCommon: bd1f35bb07924f4cc57417b00dab6734747b6423 @@ -3351,7 +3351,7 @@ SPEC CHECKSUMS: libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: b01c4b468aed88931afefbde03d790fc4f8b010a - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23 RCTDeprecation: 5eb1d2eeff5fb91151e8a8eef45b6c7658b6c897 RCTRequired: cebcf9442fc296c9b89ac791dfd463021d9f6f23 RCTTypeSafety: b99aa872829ee18f6e777e0ef55852521c5a6788 @@ -3434,6 +3434,6 @@ SPEC CHECKSUMS: Yoga: cc4a6600d61e4e9276e860d4d68eebb834a050ba ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 3afa944c1a159bdd57b3aa134ffed0193619be93 +PODFILE CHECKSUM: 1230fa854271c97af86d67b7424a7496e5f328a8 COCOAPODS: 1.16.2 From 08025f3726d247a7c56dd50f12f034b840778e6f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Thu, 26 Mar 2026 08:50:53 -0400 Subject: [PATCH 29/42] add patch so fmt change isn't lost (#29075) --- shared/patches/react-native+0.81.5.patch | 49 +++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/shared/patches/react-native+0.81.5.patch b/shared/patches/react-native+0.81.5.patch index 6044f91dc4a0..edf3f186d66a 100644 --- a/shared/patches/react-native+0.81.5.patch +++ b/shared/patches/react-native+0.81.5.patch @@ -2,7 +2,7 @@ diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmana index 216bb23..ec0578d 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -411,10 +411,11 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi +@@ -411,10 +411,11 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage CGSize attachmentSize = attachment.bounds.size; CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; @@ -18,3 +18,50 @@ index 216bb23..ec0578d 100644 auto rect = facebook::react::Rect{ facebook::react::Point{frame.origin.x, frame.origin.y}, +diff --git a/node_modules/react-native/gradle/libs.versions.toml b/node_modules/react-native/gradle/libs.versions.toml +index f0902b7..cb88f66 100644 +--- a/node_modules/react-native/gradle/libs.versions.toml ++++ b/node_modules/react-native/gradle/libs.versions.toml +@@ -43,7 +43,7 @@ yoga-proguard-annotations = "1.19.0" + boost="1_83_0" + doubleconversion="1.1.6" + fastFloat="8.0.0" +-fmt="11.0.2" ++fmt="12.1.0" + folly="2024.11.18.00" + glog="0.3.5" + gflags="2.2.0" +diff --git a/node_modules/react-native/third-party-podspecs/RCT-Folly.podspec b/node_modules/react-native/third-party-podspecs/RCT-Folly.podspec +index 8852179..040c4f0 100644 +--- a/node_modules/react-native/third-party-podspecs/RCT-Folly.podspec ++++ b/node_modules/react-native/third-party-podspecs/RCT-Folly.podspec +@@ -25,7 +25,7 @@ Pod::Spec.new do |spec| + spec.dependency "DoubleConversion" + spec.dependency "glog" + spec.dependency "fast_float", "8.0.0" +- spec.dependency "fmt", "11.0.2" ++ spec.dependency "fmt", "12.1.0" + spec.compiler_flags = '-Wno-documentation -faligned-new' + spec.source_files = 'folly/String.cpp', + 'folly/Conv.cpp', +diff --git a/node_modules/react-native/third-party-podspecs/fmt.podspec b/node_modules/react-native/third-party-podspecs/fmt.podspec +index 2f38990..a40c575 100644 +--- a/node_modules/react-native/third-party-podspecs/fmt.podspec ++++ b/node_modules/react-native/third-party-podspecs/fmt.podspec +@@ -8,14 +8,14 @@ fmt_git_url = fmt_config[:git] + + Pod::Spec.new do |spec| + spec.name = "fmt" +- spec.version = "11.0.2" ++ spec.version = "12.1.0" + spec.license = { :type => "MIT" } + spec.homepage = "https://github.com/fmtlib/fmt" + spec.summary = "{fmt} is an open-source formatting library for C++. It can be used as a safe and fast alternative to (s)printf and iostreams." + spec.authors = "The fmt contributors" + spec.source = { + :git => fmt_git_url, +- :tag => "11.0.2" ++ :tag => "12.1.0" + } + spec.pod_target_xcconfig = { + "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), From 664ebbfaa3e00d3e5c7040ec92876345d6478c38 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:28:46 -0400 Subject: [PATCH 30/42] CI: clean yarn cache (#29079) --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index c6fbbd5e0a45..dbe456d6356d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -203,6 +203,7 @@ helpers.rootLinuxNode(env, { sh "go install mvdan.cc/gofumpt" } dir ('protocol') { + sh "yarn cache clean avdl-compiler" sh "yarn --frozen-lockfile" sh "make clean" sh "make" From d8f16d1b20ca04a3fd5adb9b4828dd01c8f19d09 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:45:54 -0400 Subject: [PATCH 31/42] bump golang.org/x/image 0.38.0 (#29077) --- go/go.mod | 2 +- go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/go.mod b/go/go.mod index 38a719ab678c..d63e916b519e 100644 --- a/go/go.mod +++ b/go/go.mod @@ -72,7 +72,7 @@ require ( github.com/vividcortex/ewma v1.1.2-0.20170804035156-43880d236f69 go.uber.org/zap v1.24.0 golang.org/x/crypto v0.49.0 - golang.org/x/image v0.34.0 + golang.org/x/image v0.38.0 golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 // indirect golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 diff --git a/go/go.sum b/go/go.sum index 6d01e32bfabe..67bfd5283537 100644 --- a/go/go.sum +++ b/go/go.sum @@ -530,8 +530,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= -golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 h1:Cr6kbEvA6nqvdHynE4CtVKlqpZB9dS1Jva/6IsHA19g= golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294/go.mod h1:RdZ+3sb4CVgpCFnzv+I4haEpwqFfsfzlLHs3L7ok+e0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= From 005a154d02a8229788a73c413a747b513842e536 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:10:18 -0400 Subject: [PATCH 32/42] Startup timing (#29078) --- go/engine/bootstrap.go | 4 +- go/libkb/secret_store.go | 6 ++- go/libkb/secret_store_darwin.go | 3 +- go/service/config.go | 30 +++++++------- go/status/status.go | 38 +++++++++++------ go/uidmap/uidmap.go | 1 + shared/constants/daemon/index.tsx | 6 +++ shared/ios/Keybase/AppDelegate.swift | 5 ++- shared/ios/Keybase/MainThreadWatchdog.swift | 45 +++++++++++++++------ 9 files changed, 94 insertions(+), 44 deletions(-) diff --git a/go/engine/bootstrap.go b/go/engine/bootstrap.go index a41e089e5cc9..d6ede82d3da2 100644 --- a/go/engine/bootstrap.go +++ b/go/engine/bootstrap.go @@ -44,6 +44,7 @@ func (e *Bootstrap) SubConsumers() []libkb.UIConsumer { } func (e *Bootstrap) lookupFullname(m libkb.MetaContext, uv keybase1.UserVersion) { + defer m.Trace("Bootstrap.lookupFullname", nil)() pkgs, err := m.G().UIDMapper.MapUIDsToUsernamePackagesOffline(m.Ctx(), m.G(), []keybase1.UID{uv.Uid}, time.Duration(0)) if err != nil { m.Warning("UID -> Username failed lookup: %s", err) @@ -62,7 +63,8 @@ func (e *Bootstrap) lookupFullname(m libkb.MetaContext, uv keybase1.UserVersion) } // Run starts the engine. -func (e *Bootstrap) Run(m libkb.MetaContext) error { +func (e *Bootstrap) Run(m libkb.MetaContext) (err error) { + defer m.Trace("Bootstrap.Run", &err)() e.status.Registered = e.signedUp(m) // if any Login engine worked previously, then ActiveDevice will diff --git a/go/libkb/secret_store.go b/go/libkb/secret_store.go index 8427edc54ca5..942d66748968 100644 --- a/go/libkb/secret_store.go +++ b/go/libkb/secret_store.go @@ -112,7 +112,8 @@ func NewSecretStore(m MetaContext, username NormalizedUsername) SecretStore { return nil } -func GetConfiguredAccountsFromProvisionedUsernames(m MetaContext, s SecretStoreAll, currentUsername NormalizedUsername, allUsernames []NormalizedUsername) ([]keybase1.ConfiguredAccount, error) { +func GetConfiguredAccountsFromProvisionedUsernames(m MetaContext, s SecretStoreAll, currentUsername NormalizedUsername, allUsernames []NormalizedUsername) (_ []keybase1.ConfiguredAccount, err error) { + defer m.Trace("GetConfiguredAccountsFromProvisionedUsernames", &err)() if !currentUsername.IsNil() { allUsernames = append(allUsernames, currentUsername) } @@ -321,7 +322,8 @@ func (s *SecretStoreLocked) ClearSecret(m MetaContext, username NormalizedUserna return s.disk.ClearSecret(m, username) } -func (s *SecretStoreLocked) GetUsersWithStoredSecrets(m MetaContext) ([]string, error) { +func (s *SecretStoreLocked) GetUsersWithStoredSecrets(m MetaContext) (_ []string, err error) { + defer m.Trace("SecretStoreLocked.GetUsersWithStoredSecrets", &err)() if s == nil || s.isNil() { return nil, nil } diff --git a/go/libkb/secret_store_darwin.go b/go/libkb/secret_store_darwin.go index e0b09aaa2703..fa3ee171684b 100644 --- a/go/libkb/secret_store_darwin.go +++ b/go/libkb/secret_store_darwin.go @@ -217,7 +217,8 @@ func HasSecretStore() bool { return true } -func (k KeychainSecretStore) GetUsersWithStoredSecrets(mctx MetaContext) ([]string, error) { +func (k KeychainSecretStore) GetUsersWithStoredSecrets(mctx MetaContext) (_ []string, err error) { + defer mctx.Trace("KeychainSecretStore.GetUsersWithStoredSecrets", &err)() accounts, err := keychain.GetAccountsForService(k.serviceName(mctx)) if err != nil { mctx.Debug("KeychainSecretStore.GetUsersWithStoredSecrets() error: %s", err) diff --git a/go/service/config.go b/go/service/config.go index 0a6ddb4cfcde..2e9d0a1aabca 100644 --- a/go/service/config.go +++ b/go/service/config.go @@ -348,22 +348,22 @@ func (h ConfigHandler) WaitForClient(_ context.Context, arg keybase1.WaitForClie return h.G().ConnectionManager.WaitForClientType(arg.ClientType, arg.Timeout.Duration()), nil } -func (h ConfigHandler) GetBootstrapStatus(ctx context.Context, sessionID int) (keybase1.BootstrapStatus, error) { +func (h ConfigHandler) GetBootstrapStatus(ctx context.Context, sessionID int) (res keybase1.BootstrapStatus, err error) { + m := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") + defer m.Trace("GetBootstrapStatus", &err)() eng := engine.NewBootstrap(h.G()) - m := libkb.NewMetaContext(ctx, h.G()) - if err := engine.RunEngine2(m, eng); err != nil { - return keybase1.BootstrapStatus{}, err + if err = engine.RunEngine2(m, eng); err != nil { + return res, err } - status := eng.Status() - h.G().Log.CDebugf(ctx, "GetBootstrapStatus: attempting to get HTTP server address") + res = eng.Status() + m.Debug("GetBootstrapStatus: attempting to get HTTP server address") for i := 0; i < 40; i++ { // wait at most 2 seconds - addr, err := h.svc.httpSrv.Addr() - if err != nil { - h.G().Log.CDebugf(ctx, "GetBootstrapStatus: failed to get HTTP server address: %s", err) + addr, addrErr := h.svc.httpSrv.Addr() + if addrErr != nil { + m.Debug("GetBootstrapStatus: failed to get HTTP server address: %s", addrErr) } else { - h.G().Log.CDebugf(ctx, "GetBootstrapStatus: http server: addr: %s token: %s", addr, - h.svc.httpSrv.Token()) - status.HttpSrvInfo = &keybase1.HttpSrvInfo{ + m.Debug("GetBootstrapStatus: http server: addr: %s token: %s", addr, h.svc.httpSrv.Token()) + res.HttpSrvInfo = &keybase1.HttpSrvInfo{ Address: addr, Token: h.svc.httpSrv.Token(), } @@ -371,10 +371,10 @@ func (h ConfigHandler) GetBootstrapStatus(ctx context.Context, sessionID int) (k } time.Sleep(50 * time.Millisecond) } - if status.HttpSrvInfo == nil { - h.G().Log.CDebugf(ctx, "GetBootstrapStatus: failed to get HTTP srv info after max attempts") + if res.HttpSrvInfo == nil { + m.Debug("GetBootstrapStatus: failed to get HTTP srv info after max attempts") } - return status, nil + return res, nil } func (h ConfigHandler) RequestFollowingAndUnverifiedFollowers(ctx context.Context, sessionID int) error { diff --git a/go/status/status.go b/go/status/status.go index eee83f10ae7c..1c1f7b8ba621 100644 --- a/go/status/status.go +++ b/go/status/status.go @@ -119,11 +119,22 @@ func GetExtendedStatus(mctx libkb.MetaContext) (res keybase1.ExtendedStatus, err res.DeviceEkNames = dekNames } - res.LocalDbStats = strings.Split(g.LocalDb.Stats(), "\n") - res.LocalChatDbStats = strings.Split(g.LocalChatDb.Stats(), "\n") - if cacheSizeInfo, err := CacheSizeInfo(g); err == nil { - res.CacheDirSizeInfo = cacheSizeInfo - } + func() { + defer mctx.Trace("LocalDb.Stats", nil)() + res.LocalDbStats = strings.Split(g.LocalDb.Stats(), "\n") + }() + + func() { + defer mctx.Trace("LocalChatDb.Stats", nil)() + res.LocalChatDbStats = strings.Split(g.LocalChatDb.Stats(), "\n") + }() + + func() { + defer mctx.Trace("CacheSizeInfo", nil)() + if cacheSizeInfo, err := CacheSizeInfo(g); err == nil { + res.CacheDirSizeInfo = cacheSizeInfo + } + }() if g.ConnectionManager != nil { xp := g.ConnectionManager.LookupByClientType(keybase1.ClientType_KBFS) @@ -134,13 +145,16 @@ func GetExtendedStatus(mctx libkb.MetaContext) (res keybase1.ExtendedStatus, err Cli: rpc.NewClient( xp, libkb.NewContextifiedErrorUnwrapper(g), nil), } - stats, err := cli.SimpleFSGetStats(mctx.Ctx()) - if err != nil { - mctx.Debug("| KBFS stats error: %+v", err) - } else { - res.LocalBlockCacheDbStats = stats.BlockCacheDbStats - res.LocalSyncCacheDbStats = stats.SyncCacheDbStats - } + func() { + defer mctx.Trace("SimpleFSGetStats", nil)() + stats, err := cli.SimpleFSGetStats(mctx.Ctx()) + if err != nil { + mctx.Debug("| KBFS stats error: %+v", err) + } else { + res.LocalBlockCacheDbStats = stats.BlockCacheDbStats + res.LocalSyncCacheDbStats = stats.SyncCacheDbStats + } + }() } } diff --git a/go/uidmap/uidmap.go b/go/uidmap/uidmap.go index c2a8594d2b5a..24894383ab70 100644 --- a/go/uidmap/uidmap.go +++ b/go/uidmap/uidmap.go @@ -355,6 +355,7 @@ func (u *UIDMap) MapUIDsToUsernamePackages(ctx context.Context, g libkb.UIDMappe uids []keybase1.UID, fullNameFreshness, networkTimeBudget time.Duration, forceNetworkForFullNames bool, ) (res []libkb.UsernamePackage, err error) { + defer libkb.CTrace(ctx, g.GetLog(), "UIDMap.MapUIDsToUsernamePackages", &err, g.GetClock())() u.Lock() defer u.Unlock() diff --git a/shared/constants/daemon/index.tsx b/shared/constants/daemon/index.tsx index c45057ab85ef..08f0bc225ced 100644 --- a/shared/constants/daemon/index.tsx +++ b/shared/constants/daemon/index.tsx @@ -175,7 +175,10 @@ export const useDaemonState = Z.createZustand((set, get) => { const f = async () => { const {setBootstrap} = storeRegistry.getState('current-user').dispatch const {setDefaultUsername} = storeRegistry.getState('config').dispatch + const tBootstrap = Date.now() + logger.info('[Bootstrap] configGetBootstrapStatus: starting') const s = await T.RPCGen.configGetBootstrapStatusRpcPromise() + logger.info(`[Bootstrap] configGetBootstrapStatus: done in ${Date.now() - tBootstrap}ms`) const {userReacjis, deviceName, deviceID, uid, loggedIn, username} = s setBootstrap({deviceID, deviceName, uid, username}) if (username) { @@ -209,7 +212,10 @@ export const useDaemonState = Z.createZustand((set, get) => { }, onRestartHandshakeNative: _onRestartHandshakeNative, refreshAccounts: async () => { + const tAccounts = Date.now() + logger.info('[Bootstrap] loginGetConfiguredAccounts: starting') const configuredAccounts = (await T.RPCGen.loginGetConfiguredAccountsRpcPromise()) ?? [] + logger.info(`[Bootstrap] loginGetConfiguredAccounts: done in ${Date.now() - tAccounts}ms`) // already have one? const {defaultUsername} = storeRegistry.getState('config') const {setAccounts, setDefaultUsername} = storeRegistry.getState('config').dispatch diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 463d6f648bb6..b5dcfa86c2ae 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -46,6 +46,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID var startupLogFileHandle: FileHandle? private var watchdog: MainThreadWatchdog? + private var bgEnterWallTime: CFAbsoluteTime = 0 public override func application( _ application: UIApplication, @@ -398,6 +399,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationDidEnterBackground(_ application: UIApplication) { + bgEnterWallTime = CFAbsoluteTimeGetCurrent() watchdog?.start(context: "background entered") application.ignoreSnapshotOnNextApplicationLaunch() NSLog("applicationDidEnterBackground: cancelling outstanding animations...") @@ -469,7 +471,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationWillEnterForeground(_ application: UIApplication) { - NSLog("applicationWillEnterForeground: start, hiding keyz screen.") + let bgDuration = CFAbsoluteTimeGetCurrent() - bgEnterWallTime + NSLog("applicationWillEnterForeground: start, hiding keyz screen (%.1fs since background)", bgDuration) hideCover() NSLog("applicationWillEnterForeground: done") } diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index d88a1f1c09c2..b6fff1a9265c 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -1,10 +1,11 @@ -import UIKit import Darwin +import UIKit // File-scope globals required by the SIGUSR1 signal handler. // Signal handlers cannot safely reference Swift objects, so these must be C-compatible globals. private let kMaxStackFrames: Int32 = 128 -private var gMainStackFrames = [UnsafeMutableRawPointer?](repeating: nil, count: Int(kMaxStackFrames)) +private var gMainStackFrames = [UnsafeMutableRawPointer?]( + repeating: nil, count: Int(kMaxStackFrames)) private var gMainStackFrameCount: Int32 = 0 private var gMainStackReady: Bool = false @@ -98,8 +99,14 @@ class MainThreadWatchdog { // 2. Cold-start or foreground watchdog: use the 30s threshold as before. if blockDuration > 30.0 || (isBackgroundContext && blockDuration >= 3.0) { let bgElapsedSec = now - bgEnterTime - let msg = String(format: "Watchdog: process resumed after %.0fs suspension (%.0fs since background)", blockDuration, bgElapsedSec) + let msg = String( + format: "Watchdog: process resumed after %.0fs suspension (%.0fs since background)", + blockDuration, bgElapsedSec) NSLog("[Startup] %@", msg) + // Capture a stack trace even on suspected suspension: if the main thread is actually + // hung during foreground re-entry (not truly suspended by iOS), the trace will show it. + // If truly suspended the signal will time out and log "stack capture timed out". + captureAndLogStackTrace() lock.lock() self.lastPong = now lock.unlock() @@ -120,7 +127,10 @@ class MainThreadWatchdog { // evolves (e.g. keychain IPC → rendering → idle) rather than a single snapshot. if lastLogTime == 0 || (now - lastLogTime) >= 1.0 { let bgElapsedSec = now - bgEnterTime - let msg = String(format: "Watchdog: main thread blocked %.1fs after foreground resume (%.0fs since background, %.0fms since launch)", blockDuration, bgElapsedSec, totalElapsedMs) + let msg = String( + format: + "Watchdog: main thread blocked %.1fs after foreground resume (%.0fs since background, %.0fms since launch)", + blockDuration, bgElapsedSec, totalElapsedMs) NSLog("[Startup] %@", msg) // Enqueue a write for when the main thread recovers DispatchQueue.main.async { [weak self] in @@ -133,7 +143,9 @@ class MainThreadWatchdog { } else { if lastLogTime != 0 { let bgElapsedSec = now - bgEnterTime - let msg = String(format: "Watchdog: main thread unblocked (%.0fs since background, %.0fms since launch)", bgElapsedSec, totalElapsedMs) + let msg = String( + format: "Watchdog: main thread unblocked (%.0fs since background, %.0fms since launch)", + bgElapsedSec, totalElapsedMs) NSLog("[Startup] %@", msg) DispatchQueue.main.async { [weak self] in self?.writeLog(msg) @@ -153,12 +165,15 @@ class MainThreadWatchdog { } // Send SIGUSR1 to the main thread, wait briefly for the handler to run, then log the stack. - // Frames are collected synchronously here on the watchdog thread, then dispatched to the main - // thread via writeLog so they appear in ios.log (captured by logsend). + // Frames are written via NSLog immediately (so they survive if the app is killed before the + // main thread recovers) and also dispatched to writeLog for ios.log / logsend. private func captureAndLogStackTrace() { gMainStackReady = false guard let tid = mainThreadPthread else { - DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: main thread pthread not captured") } + NSLog("[Startup] Watchdog: main thread pthread not captured") + DispatchQueue.main.async { [weak self] in + self?.writeLog("Watchdog: main thread pthread not captured") + } return } pthread_kill(tid, SIGUSR1) @@ -168,17 +183,19 @@ class MainThreadWatchdog { Thread.sleep(forTimeInterval: 0.01) } guard gMainStackReady else { - DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: stack capture timed out") } + NSLog("[Startup] Watchdog: stack capture timed out") + DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: stack capture timed out") + } return } let count = Int(gMainStackFrameCount) // Collect the binary load slide so addresses can be symbolicated offline: // atos -o Keybase.app.dSYM/Contents/Resources/DWARF/Keybase -l
let slide = _dyld_get_image_vmaddr_slide(0) - // Build the frame strings synchronously while the globals are still valid, then - // dispatch a single block to write them all once the main thread unblocks. + // Build the frame strings synchronously while the globals are still valid. var lines = [String]() - lines.append(String(format: "Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide)) + lines.append( + String(format: "Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide)) gMainStackFrames.withUnsafeMutableBufferPointer { buf in if let syms = backtrace_symbols(buf.baseAddress, Int32(count)) { for i in 0.. Date: Fri, 27 Mar 2026 15:28:41 -0400 Subject: [PATCH 33/42] restart loopback server on dial fail (#29086) --- go/bind/keybase.go | 25 ++++++++++++++++++- go/libkb/loopback.go | 2 ++ go/libkb/socket.go | 5 ++++ .../main/java/com/reactnativekb/KbModule.kt | 3 +++ rnmodules/react-native-kb/ios/Kb.mm | 4 +++ shared/ios/Keybase/AppDelegate.swift | 25 ++++++++++++++++--- 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/go/bind/keybase.go b/go/bind/keybase.go index 46a598215680..99ff0062bea6 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -526,16 +526,33 @@ func ReadArr() (data []byte, err error) { func ensureConnection() error { start := time.Now() if !isInited() { + log("ensureConnection: keybase not initialized") return errors.New("keybase not initialized") } if kbCtx == nil || kbCtx.LoopbackListener == nil { + log("ensureConnection: loopback listener not initialized (kbCtx nil: %v)", kbCtx == nil) return errors.New("loopback listener not initialized") } var err error conn, err = kbCtx.LoopbackListener.Dial() if err != nil { - return fmt.Errorf("Failed to dial loopback listener: %s", err) + // The listener was closed (isClosed=true, returns syscall.EINVAL). Recreate it and + // start a new ListenLoop goroutine, then retry the dial once. + log("ensureConnection: Dial failed (%v), restarting loopback server", err) + l, rerr := kbCtx.MakeLoopbackServer() + if rerr != nil { + log("ensureConnection: MakeLoopbackServer failed: %v", rerr) + return fmt.Errorf("failed to restart loopback server: %s", rerr) + } + go func() { _ = kbSvc.ListenLoop(l) }() + conn, err = kbCtx.LoopbackListener.Dial() + if err != nil { + log("ensureConnection: Dial failed after restart: %v", err) + return fmt.Errorf("failed to dial after loopback restart: %s", err) + } + log("ensureConnection: loopback server restarted successfully in %v", time.Since(start)) + return nil } log("Go: Established loopback connection in %v", time.Since(start)) return nil @@ -561,11 +578,17 @@ func Reset() error { // NotifyJSReady signals that the JavaScript side is ready to send/receive RPCs. // This unblocks the ReadArr loop and allows bidirectional communication. +// jsReadyCh is closed once and stays closed — repeated calls from engine resets are no-ops. func NotifyJSReady() { + notified := false jsReadyOnce.Do(func() { + notified = true log("Go: JS signaled ready, unblocking RPC communication") close(jsReadyCh) }) + if !notified { + log("Go: NotifyJSReady called again (no-op, channel already closed — engine reset?)") + } } // ForceGC Forces a gc diff --git a/go/libkb/loopback.go b/go/libkb/loopback.go index 00368cc18590..b264eb396bdc 100644 --- a/go/libkb/loopback.go +++ b/go/libkb/loopback.go @@ -102,8 +102,10 @@ func (ll *LoopbackListener) Close() (err error) { ll.mutex.Lock() defer ll.mutex.Unlock() if ll.isClosed { + ll.logCtx.GetLog().Debug("LoopbackListener.Close: already closed") return syscall.EINVAL } + ll.logCtx.GetLog().Debug("LoopbackListener.Close: closing") ll.isClosed = true close(ll.ch) return diff --git a/go/libkb/socket.go b/go/libkb/socket.go index 6a668c1a40fb..2361c6341b50 100644 --- a/go/libkb/socket.go +++ b/go/libkb/socket.go @@ -42,6 +42,11 @@ type SocketWrapper struct { func (g *GlobalContext) MakeLoopbackServer() (l net.Listener, err error) { g.socketWrapperMu.Lock() defer g.socketWrapperMu.Unlock() + if g.LoopbackListener != nil { + g.Log.Debug("MakeLoopbackServer: replacing existing loopback listener (old isClosed=%v)", g.LoopbackListener.isClosed) + } else { + g.Log.Debug("MakeLoopbackServer: creating new loopback listener") + } g.LoopbackListener = NewLoopbackListener(g) l = g.LoopbackListener return l, err diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt index d8e277578a87..7d2c0d745308 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt @@ -895,6 +895,9 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext) { } else { NativeLogger.error("Exception in ReadFromKBLib.run", e) } + // Back off on error to avoid spinning at full CPU speed when Go is + // unavailable (e.g. during init or loopback restart). + try { Thread.sleep(100) } catch (ie: InterruptedException) { Thread.currentThread().interrupt() } } } while (!Thread.currentThread().isInterrupted() && reactContext.hasActiveCatalystInstance()) } diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index 8494d25892b5..fd0d18c3313c 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -311,6 +311,7 @@ - (NSDictionary *)getConstants { } RCT_EXPORT_METHOD(engineReset) { + NSLog(@"engineReset: called (JS hot reload), resetting Go engine"); NSError *error = nil; KeybaseReset(&error); [self sendEventWithName:metaEventName body:metaEventEngineReset]; @@ -352,6 +353,9 @@ - (NSDictionary *)getConstants { NSData *data = KeybaseReadArr(&error); if (error) { NSLog(@"Error reading data: %@", error); + // Back off on error to avoid spinning at ~35K/sec and starving the main thread CPU + // during foreground re-entry (seen during hang investigation: 419K errors in 12s). + [NSThread sleepForTimeInterval:0.1]; } else if (data) { __typeof__(self) strongSelf = weakSelf; if (strongSelf) { diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index b5dcfa86c2ae..dc08f2fe4449 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -47,6 +47,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID private var watchdog: MainThreadWatchdog? private var bgEnterWallTime: CFAbsoluteTime = 0 + private var fgEnterTime: CFAbsoluteTime = 0 public override func application( _ application: UIApplication, @@ -153,9 +154,20 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID return } + // Skip writes when protected data is unavailable (device locked before first unlock). + // NSConcreteFileHandle.writeData: throws NSException on protected-file access failures, + // which Swift do-catch cannot intercept, causing a crash (see: _NSFileHandleRaiseOperationExceptionWhileReading). + guard UIApplication.shared.isProtectedDataAvailable else { + return + } + if self.startupLogFileHandle == nil { if !FileManager.default.fileExists(atPath: logFilePath) { - FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) + // Match the parent directory's protection class so the file remains accessible + // in the background after the device has been unlocked once. + FileManager.default.createFile( + atPath: logFilePath, contents: nil, + attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]) } if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { fileHandle.seekToEndOfFile() @@ -439,7 +451,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID watchdog?.stop() Keybasego.KeybaseFlushLogs() let elapsed = CFAbsoluteTimeGetCurrent() - AppDelegate.appStartTime - writeStartupTimingLog(String(format: "applicationDidBecomeActive: %.1fms after launch", elapsed * 1000)) + let fgGap = fgEnterTime > 0 ? CFAbsoluteTimeGetCurrent() - fgEnterTime : 0 + writeStartupTimingLog(String(format: "applicationDidBecomeActive: %.1fms after launch, %.0fms after willEnterForeground", elapsed * 1000, fgGap * 1000)) + NSLog("[Startup] applicationDidBecomeActive: %.0fms after willEnterForeground", fgGap * 1000) NSLog("applicationDidBecomeActive: hiding keyz screen.") hideCover() NSLog("applicationDidBecomeActive: notifying service.") @@ -471,12 +485,17 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationWillEnterForeground(_ application: UIApplication) { - let bgDuration = CFAbsoluteTimeGetCurrent() - bgEnterWallTime + fgEnterTime = CFAbsoluteTimeGetCurrent() + let bgDuration = fgEnterTime - bgEnterWallTime NSLog("applicationWillEnterForeground: start, hiding keyz screen (%.1fs since background)", bgDuration) hideCover() NSLog("applicationWillEnterForeground: done") } + public func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) { + NSLog("[Startup] applicationProtectedDataDidBecomeAvailable") + } + } class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { From 8fdf422edc7c0a52f22dfcf7229627e3b7711548 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:02:46 -0400 Subject: [PATCH 34/42] startup fixes (#29091) --- shared/ios/Keybase/AppDelegate.swift | 7 ------- shared/ios/Keybase/MainThreadWatchdog.swift | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index dc08f2fe4449..d172272138c7 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -154,13 +154,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID return } - // Skip writes when protected data is unavailable (device locked before first unlock). - // NSConcreteFileHandle.writeData: throws NSException on protected-file access failures, - // which Swift do-catch cannot intercept, causing a crash (see: _NSFileHandleRaiseOperationExceptionWhileReading). - guard UIApplication.shared.isProtectedDataAvailable else { - return - } - if self.startupLogFileHandle == nil { if !FileManager.default.fileExists(atPath: logFilePath) { // Match the parent directory's protection class so the file remains accessible diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index b6fff1a9265c..7fe68237d68c 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -36,6 +36,11 @@ class MainThreadWatchdog { // the SIGUSR1 handler that records the main thread stack on demand. func install() { mainThreadPthread = pthread_self() + // Force lazy initialization of gMainStackFrames before registering the signal handler. + // Swift file-scope globals initialize via dispatch_once on first access; triggering that + // inside a signal handler while the ObjC runtime lock is held causes + // _os_unfair_lock_recursive_abort (crash seen in PID 31439, 2026-03-28). + _ = gMainStackFrames.count signal(SIGUSR1) { _ in gMainStackFrameCount = backtrace(&gMainStackFrames, kMaxStackFrames) gMainStackReady = true From 9276ad09fe062ee8fff958cabd5302afbbbb57bf Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:31:40 -0400 Subject: [PATCH 35/42] bridge lifecycle logging (#29097) --- go/bind/keybase.go | 88 +++++++++++++++++++++++++++-- rnmodules/react-native-kb/ios/Kb.mm | 82 ++++++++++++++++++++++++--- 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/go/bind/keybase.go b/go/bind/keybase.go index 99ff0062bea6..bde063442eec 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "os" "path/filepath" @@ -16,6 +17,7 @@ import ( "runtime/trace" "strings" "sync" + "sync/atomic" "time" "github.com/keybase/client/go/chat/globals" @@ -59,6 +61,63 @@ var ( connMutex sync.Mutex // Protects conn operations ) +var ( + connGeneration atomic.Uint64 + resetGeneration atomic.Uint64 + jsReadySignalCount atomic.Uint64 + writeErrCount atomic.Uint64 + readErrCount atomic.Uint64 + readErrStreak atomic.Uint64 + readErrStreakStart atomic.Int64 +) + +func describeConn(c net.Conn) string { + if c == nil { + return "" + } + return fmt.Sprintf("%T@%p", c, c) +} + +func describeErr(err error) string { + if err == nil { + return "" + } + return fmt.Sprintf("%T: %v", err, err) +} + +func appStateForLog() string { + if kbCtx == nil || kbCtx.MobileAppState == nil { + return "" + } + return fmt.Sprintf("%v", kbCtx.MobileAppState.State()) +} + +func noteReadSuccess(c net.Conn, n int) { + streak := readErrStreak.Swap(0) + startUnix := readErrStreakStart.Swap(0) + if streak == 0 { + return + } + var dur time.Duration + if startUnix > 0 { + dur = time.Since(time.Unix(0, startUnix)) + } + log("Go: ReadArr recovered after streak=%d conn=%s nextReadBytes=%d appState=%s duration=%s", + streak, describeConn(c), n, appStateForLog(), dur) +} + +func noteReadError(c net.Conn, err error) { + total := readErrCount.Add(1) + streak := readErrStreak.Add(1) + if streak == 1 { + readErrStreakStart.Store(time.Now().UnixNano()) + } + if streak <= 5 || streak == 10 || streak%50 == 0 { + log("Go: ReadArr error streak=%d total=%d conn=%s appState=%s err=%s eof=%v", + streak, total, describeConn(c), appStateForLog(), describeErr(err), errors.Is(err, io.EOF)) + } +} + // log writes to kbCtx.Log if available, otherwise falls back to fmt.Printf func log(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) @@ -469,9 +528,14 @@ func WriteArr(b []byte) (err error) { n, err := currentConn.Write(bytes) if err != nil { + total := writeErrCount.Add(1) + log("Go: WriteArr error total=%d conn=%s len=%d appState=%s err=%s", + total, describeConn(currentConn), len(bytes), appStateForLog(), describeErr(err)) return fmt.Errorf("Write error: %s", err) } if n != len(bytes) { + log("Go: WriteArr short write conn=%s wrote=%d expected=%d appState=%s", + describeConn(currentConn), n, len(bytes), appStateForLog()) return errors.New("Did not write all the data") } return nil @@ -507,10 +571,12 @@ func ReadArr() (data []byte, err error) { n, err := currentConn.Read(buffer) if n > 0 && err == nil { + noteReadSuccess(currentConn, n) return buffer[0:n], nil } if err != nil { + noteReadError(currentConn, err) // Attempt to fix the connection if ierr := Reset(); ierr != nil { log("failed to Reset: %v", ierr) @@ -535,6 +601,8 @@ func ensureConnection() error { } var err error + log("ensureConnection: attempting dial listener=%T@%p existingConn=%s appState=%s", + kbCtx.LoopbackListener, kbCtx.LoopbackListener, describeConn(conn), appStateForLog()) conn, err = kbCtx.LoopbackListener.Dial() if err != nil { // The listener was closed (isClosed=true, returns syscall.EINVAL). Recreate it and @@ -551,10 +619,14 @@ func ensureConnection() error { log("ensureConnection: Dial failed after restart: %v", err) return fmt.Errorf("failed to dial after loopback restart: %s", err) } - log("ensureConnection: loopback server restarted successfully in %v", time.Since(start)) + gen := connGeneration.Add(1) + log("ensureConnection: loopback server restarted successfully in %v gen=%d conn=%s appState=%s", + time.Since(start), gen, describeConn(conn), appStateForLog()) return nil } - log("Go: Established loopback connection in %v", time.Since(start)) + gen := connGeneration.Add(1) + log("Go: Established loopback connection in %v gen=%d conn=%s appState=%s", + time.Since(start), gen, describeConn(conn), appStateForLog()) return nil } @@ -563,16 +635,19 @@ func Reset() error { connMutex.Lock() defer connMutex.Unlock() + resetID := resetGeneration.Add(1) + log("Go: Reset #%d start conn=%s appState=%s", resetID, describeConn(conn), appStateForLog()) if conn != nil { conn.Close() conn = nil } if kbCtx == nil || kbCtx.LoopbackListener == nil { + log("Go: Reset #%d complete without listener appState=%s", resetID, appStateForLog()) return nil } // Connection will be re-established lazily on next read/write - log("Go: Connection reset, will reconnect on next operation") + log("Go: Connection reset, will reconnect on next operation (reset=%d appState=%s)", resetID, appStateForLog()) return nil } @@ -580,14 +655,17 @@ func Reset() error { // This unblocks the ReadArr loop and allows bidirectional communication. // jsReadyCh is closed once and stays closed — repeated calls from engine resets are no-ops. func NotifyJSReady() { + call := jsReadySignalCount.Add(1) notified := false jsReadyOnce.Do(func() { notified = true - log("Go: JS signaled ready, unblocking RPC communication") + log("Go: JS signaled ready, unblocking RPC communication (call=%d appState=%s conn=%s)", + call, appStateForLog(), describeConn(conn)) close(jsReadyCh) }) if !notified { - log("Go: NotifyJSReady called again (no-op, channel already closed — engine reset?)") + log("Go: NotifyJSReady called again (no-op, channel already closed — engine reset?) call=%d appState=%s conn=%s", + call, appStateForLog(), describeConn(conn)) } } diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index fd0d18c3313c..6f786f7ab22d 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -12,6 +12,7 @@ #import #import #import +#include #import #import #import @@ -24,12 +25,14 @@ using namespace std; using namespace kb; +extern void *currentRuntime; + // used to keep track of objects getting destroyed on the js side class KBTearDown : public jsi::HostObject { public: KBTearDown() { Tearup(); } virtual ~KBTearDown() { - NSLog(@"KBTeardown!!!"); + NSLog(@"KBTeardown!!! currentRuntime=%p", currentRuntime); Teardown(); } virtual jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) { @@ -101,6 +104,13 @@ @implementation Kb // sanity check the runtime isn't out of sync due to reload etc void *currentRuntime = nil; +std::atomic gKbInstallCount{0}; +std::atomic gNotifyJSReadyCount{0}; +std::atomic gReadLoopGeneration{0}; + +static const char *KBStringOrNil(NSString *value) { + return value ? value.UTF8String : ""; +} RCT_EXPORT_MODULE() @@ -160,6 +170,8 @@ + (void)handlePastedImages:(NSArray *)images { } - (void)invalidate { + NSLog(@"Kb.invalidate: self=%p bridge=%p readQueue=%p jsRuntime=%p currentRuntime=%p scheduler=%p", + self, self.bridge, self.readQueue, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); [[NSNotificationCenter defaultCenter] removeObserver:self]; currentRuntime = nil; _jsRuntime = nil; @@ -200,6 +212,14 @@ - (void)sendToJS:(NSData *)data { NSLog(@"Failed to find jsi in sendToJS invokeAsync!!!"); return; } + if (currentRuntime && currentRuntime != jsRuntimePtr) { + NSLog(@"sendToJS: stored runtime mismatch self=%p bridge=%p storedRuntime=%p currentRuntime=%p callbackRuntime=%p", + strongSelf, strongSelf.bridge, jsRuntimePtr, currentRuntime, &jsiRuntime); + } + if (currentRuntime && currentRuntime != &jsiRuntime) { + NSLog(@"sendToJS: callback runtime mismatch self=%p bridge=%p storedRuntime=%p currentRuntime=%p callbackRuntime=%p", + strongSelf, strongSelf.bridge, jsRuntimePtr, currentRuntime, &jsiRuntime); + } int size = (int)[data length]; if (size <= 0) { @@ -311,7 +331,8 @@ - (NSDictionary *)getConstants { } RCT_EXPORT_METHOD(engineReset) { - NSLog(@"engineReset: called (JS hot reload), resetting Go engine"); + NSLog(@"engineReset: called (JS hot reload), resetting Go engine self=%p bridge=%p jsRuntime=%p currentRuntime=%p scheduler=%p", + self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); NSError *error = nil; KeybaseReset(&error); [self sendEventWithName:metaEventName body:metaEventEngineReset]; @@ -322,10 +343,15 @@ - (NSDictionary *)getConstants { RCT_EXPORT_METHOD(notifyJSReady) { __weak __typeof__(self) weakSelf = self; + uint64_t notifyCount = gNotifyJSReadyCount.fetch_add(1) + 1; - NSLog(@"notifyJSReady: called from JS, queuing main thread block"); + NSLog(@"notifyJSReady[%llu]: called from JS, queuing main thread block self=%p bridge=%p jsRuntime=%p currentRuntime=%p scheduler=%p", + (unsigned long long)notifyCount, self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); dispatch_async(dispatch_get_main_queue(), ^{ - NSLog(@"notifyJSReady: main thread block executing"); + uint64_t readLoopGen = gReadLoopGeneration.fetch_add(1) + 1; + NSLog(@"notifyJSReady[%llu]: main thread block executing self=%p bridge=%p jsRuntime=%p currentRuntime=%p nextReadLoop=%llu", + (unsigned long long)notifyCount, self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, + (unsigned long long)readLoopGen); // Setup infrastructure [[NSNotificationCenter defaultCenter] addObserver:self @@ -336,15 +362,25 @@ - (NSDictionary *)getConstants { // Signal to Go that JS is ready KeybaseNotifyJSReady(); - NSLog(@"Notified Go that JS is ready, starting ReadArr loop"); + NSLog(@"notifyJSReady[%llu]: Notified Go that JS is ready, starting ReadArr loop=%llu queue=%p", + (unsigned long long)notifyCount, (unsigned long long)readLoopGen, self.readQueue); // Start the read loop dispatch_async(self.readQueue, ^{ + uint64_t consecutiveErrors = 0; + uint64_t totalErrors = 0; + CFAbsoluteTime errorStreakStart = 0; + NSLog(@"ReadArr loop[%llu] start self=%p bridge=%p readQueue=%p jsRuntime=%p currentRuntime=%p scheduler=%p", + (unsigned long long)readLoopGen, self, self.bridge, self.readQueue, [self javaScriptRuntimePointer], + currentRuntime, jsScheduler.get()); while (true) { { __typeof__(self) strongSelf = weakSelf; if (!strongSelf || !strongSelf.bridge) { - NSLog(@"Bridge dead, bailing from ReadArr loop"); + NSLog(@"ReadArr loop[%llu] exit: bridge dead self=%p bridge=%p totalErrors=%llu consecutiveErrors=%llu jsRuntime=%p currentRuntime=%p", + (unsigned long long)readLoopGen, strongSelf, strongSelf.bridge, (unsigned long long)totalErrors, + (unsigned long long)consecutiveErrors, + strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); return; } } @@ -352,11 +388,33 @@ - (NSDictionary *)getConstants { NSError *error = nil; NSData *data = KeybaseReadArr(&error); if (error) { - NSLog(@"Error reading data: %@", error); + totalErrors++; + consecutiveErrors++; + if (errorStreakStart == 0) { + errorStreakStart = CFAbsoluteTimeGetCurrent(); + } + if (consecutiveErrors <= 5 || consecutiveErrors % 50 == 0) { + __typeof__(self) strongSelf = weakSelf; + NSLog(@"ReadArr loop[%llu] error streak=%llu total=%llu domain=%s code=%ld desc=%s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", + (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, + (unsigned long long)totalErrors, KBStringOrNil(error.domain), (long)error.code, + KBStringOrNil(error.localizedDescription), strongSelf, strongSelf.bridge, + strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); + } // Back off on error to avoid spinning at ~35K/sec and starving the main thread CPU // during foreground re-entry (seen during hang investigation: 419K errors in 12s). [NSThread sleepForTimeInterval:0.1]; } else if (data) { + if (consecutiveErrors > 0) { + CFAbsoluteTime streakDuration = errorStreakStart > 0 ? CFAbsoluteTimeGetCurrent() - errorStreakStart : 0; + __typeof__(self) strongSelf = weakSelf; + NSLog(@"ReadArr loop[%llu] recovered after streak=%llu total=%llu duration=%.3fs nextBytes=%lu self=%p bridge=%p jsRuntime=%p currentRuntime=%p", + (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, + (unsigned long long)totalErrors, streakDuration, (unsigned long)data.length, + strongSelf, strongSelf.bridge, strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); + consecutiveErrors = 0; + errorStreakStart = 0; + } __typeof__(self) strongSelf = weakSelf; if (strongSelf) { [strongSelf sendToJS:data]; @@ -371,9 +429,19 @@ - (NSDictionary *)getConstants { RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; + void *previousRuntime = currentRuntime; _jsRuntime = (jsi::Runtime *)cxxBridge.runtime; + currentRuntime = cxxBridge.runtime; auto &rnRuntime = *(jsi::Runtime *)cxxBridge.runtime; jsScheduler = std::make_shared(rnRuntime, _callInvoker.callInvoker); + uint64_t installCount = gKbInstallCount.fetch_add(1) + 1; + NSLog(@"install[%llu]: self=%p bridge=%p runtime=%p previousRuntime=%p scheduler=%p callInvoker=%p", + (unsigned long long)installCount, self, self.bridge, cxxBridge.runtime, previousRuntime, jsScheduler.get(), + _callInvoker.callInvoker.get()); + if (previousRuntime && previousRuntime != cxxBridge.runtime) { + NSLog(@"install[%llu]: runtime changed without a matching invalidate? previousRuntime=%p newRuntime=%p bridge=%p", + (unsigned long long)installCount, previousRuntime, cxxBridge.runtime, self.bridge); + } // stash the current runtime to keep in sync auto rpcOnGoWrap = [](Runtime &runtime, const Value &thisValue, const Value *arguments, size_t count) -> Value { From 00b32db4c5292673f6138f093634ac7bc7cb2892 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:53:15 -0400 Subject: [PATCH 36/42] more debug logging (#29100) --- go/bind/keybase.go | 6 +- rnmodules/react-native-kb/ios/Kb.mm | 11 +- shared/ios/Keybase/AppDelegate.swift | 179 ++++++++++++++++++--------- 3 files changed, 131 insertions(+), 65 deletions(-) diff --git a/go/bind/keybase.go b/go/bind/keybase.go index bde063442eec..f9f4695884e4 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -118,13 +118,15 @@ func noteReadError(c net.Conn, err error) { } } -// log writes to kbCtx.Log if available, otherwise falls back to fmt.Printf +// log writes to kbCtx.Log if available, otherwise falls back to stderr. +// Stderr is captured in crash logs and the Xcode console, making early Init +// messages (before kbCtx.Log is set up by Configure) visible in diagnostics. func log(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) if kbCtx != nil && kbCtx.Log != nil { kbCtx.Log.Info(msg) } else { - fmt.Printf("%s\n", msg) + fmt.Fprintf(os.Stderr, "keybase: %s\n", msg) } } diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index 6f786f7ab22d..0673eaf9aaee 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import #include #import @@ -395,11 +396,11 @@ - (NSDictionary *)getConstants { } if (consecutiveErrors <= 5 || consecutiveErrors % 50 == 0) { __typeof__(self) strongSelf = weakSelf; - NSLog(@"ReadArr loop[%llu] error streak=%llu total=%llu domain=%s code=%ld desc=%s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", - (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, - (unsigned long long)totalErrors, KBStringOrNil(error.domain), (long)error.code, - KBStringOrNil(error.localizedDescription), strongSelf, strongSelf.bridge, - strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); + os_log(OS_LOG_DEFAULT, "ReadArr loop[%llu] error streak=%llu total=%llu domain=%{public}s code=%ld desc=%{public}s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", + (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, + (unsigned long long)totalErrors, KBStringOrNil(error.domain), (long)error.code, + KBStringOrNil(error.localizedDescription), strongSelf, strongSelf.bridge, + strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); } // Back off on error to avoid spinning at ~35K/sec and starving the main thread CPU // during foreground re-entry (seen during hang investigation: 419K errors in 12s). diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index d172272138c7..a60f71c391af 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -1,12 +1,13 @@ +import AVFoundation import Expo +import ExpoModulesCore +import KBCommon +import Keybasego +import OSLog import React import ReactAppDependencyProvider -import KBCommon import UIKit import UserNotifications -import AVFoundation -import ExpoModulesCore -import Keybasego class KeyboardWindow: UIWindow { override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { @@ -17,13 +18,15 @@ class KeyboardWindow: UIWindow { if key.keyCode == .keyboardReturnOrEnter { if key.modifierFlags.contains(.shift) { - NotificationCenter.default.post(name: NSNotification.Name("hardwareKeyPressed"), - object: nil, - userInfo: ["pressedKey": "shift-enter"]) + NotificationCenter.default.post( + name: NSNotification.Name("hardwareKeyPressed"), + object: nil, + userInfo: ["pressedKey": "shift-enter"]) } else { - NotificationCenter.default.post(name: NSNotification.Name("hardwareKeyPressed"), - object: nil, - userInfo: ["pressedKey": "enter"]) + NotificationCenter.default.post( + name: NSNotification.Name("hardwareKeyPressed"), + object: nil, + userInfo: ["pressedKey": "enter"]) } return } @@ -33,7 +36,9 @@ class KeyboardWindow: UIWindow { } @UIApplicationMain -public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UIDropInteractionDelegate { +public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, + UIDropInteractionDelegate +{ var window: UIWindow? var reactNativeDelegate: ExpoReactNativeFactoryDelegate? @@ -62,9 +67,11 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID FsPathsHolder.shared().fsPaths = self.fsPaths self.writeStartupTimingLog("didFinishLaunchingWithOptions start") - let wd = MainThreadWatchdog(appStartTime: AppDelegate.appStartTime, writeLog: { [weak self] msg in - self?.writeStartupTimingLog(msg) - }) + let wd = MainThreadWatchdog( + appStartTime: AppDelegate.appStartTime, + writeLog: { [weak self] msg in + self?.writeStartupTimingLog(msg) + }) wd.start(context: "cold start") self.watchdog = wd @@ -75,11 +82,14 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID wd.install() if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { - let notificationDict = Dictionary(uniqueKeysWithValues: remoteNotification.map { (String(describing: $0.key), $0.value) }) + let notificationDict = Dictionary( + uniqueKeysWithValues: remoteNotification.map { (String(describing: $0.key), $0.value) }) KbSetInitialNotification(notificationDict) } - NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main) { [weak self] notification in + NotificationCenter.default.addObserver( + forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main + ) { [weak self] notification in NSLog("Memory warning received - deferring GC during React Native initialization") // see if this helps avoid this crash DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { @@ -98,13 +108,13 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID reactNativeFactory = factory bindReactNativeFactory(factory) -#if os(iOS) || os(tvOS) - window = KeyboardWindow(frame: UIScreen.main.bounds) - factory.startReactNative( - withModuleName: "Keybase", - in: window, - launchOptions: launchOptions) -#endif + #if os(iOS) || os(tvOS) + window = KeyboardWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "Keybase", + in: window, + launchOptions: launchOptions) + #endif self.writeStartupTimingLog("After RN init") self.closeStartupLogFile() @@ -125,7 +135,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { - return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + return super.application(app, open: url, options: options) + || RCTLinkingManager.application(app, open: url, options: options) } // Universal Links @@ -134,8 +145,10 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { - let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) - return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + let result = RCTLinkingManager.application( + application, continue: userActivity, restorationHandler: restorationHandler) + return super.application( + application, continue: userActivity, restorationHandler: restorationHandler) || result } /////// KB specific @@ -182,7 +195,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let timestamp = String(format: "%@.%06dZ", dateString, microseconds) let fileName = URL(fileURLWithPath: file).lastPathComponent - let logMessage = String(format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message) + let logMessage = String( + format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message + ) guard let logData = logMessage.data(using: .utf8) else { return @@ -217,11 +232,11 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let isIPad = UIDevice.current.userInterfaceIdiom == .pad let isIOS = true -#if targetEnvironment(simulator) - let securityAccessGroupOverride = true -#else - let securityAccessGroupOverride = false -#endif + #if targetEnvironment(simulator) + let securityAccessGroupOverride = true + #else + let securityAccessGroupOverride = false + #endif self.writeStartupTimingLog("Before Go init") @@ -229,8 +244,22 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID NSLog("Starting KeybaseInit (synchronous)...") var err: NSError? let shareIntentDonator = ShareIntentDonatorImpl() - Keybasego.KeybaseInit(self.fsPaths["homedir"], self.fsPaths["sharedHome"], self.fsPaths["logFile"], "prod", securityAccessGroupOverride, nil, nil, systemVer, isIPad, nil, isIOS, shareIntentDonator, &err) - if let err { NSLog("KeybaseInit FAILED: \(err)") } + Keybasego.KeybaseInit( + self.fsPaths["homedir"], self.fsPaths["sharedHome"], self.fsPaths["logFile"], "prod", + securityAccessGroupOverride, nil, nil, systemVer, isIPad, nil, isIOS, shareIntentDonator, &err + ) + if let err { + // Log to system log with public annotation so it's not redacted in logarchive. + os_log( + .error, log: OSLog(subsystem: "keybase", category: "init"), + "KeybaseInit FAILED: %{public}@ (code=%ld domain=%{public}@)", + err.localizedDescription, err.code, err.domain) + // Also write to ios.log so it's captured in xcappdata even without a device attached. + self.writeStartupTimingLog( + "KeybaseInit FAILED: \(err.localizedDescription) (code=\(err.code) domain=\(err.domain))") + } else { + self.writeStartupTimingLog("KeybaseInit succeeded") + } self.writeStartupTimingLog("After Go init") } @@ -272,7 +301,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.resignImageView?.image = UIImage(named: "LaunchImage") if let view = self.resignImageView { self.window?.addSubview(view) } - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + UIApplication.shared.setMinimumBackgroundFetchInterval( + UIApplication.backgroundFetchIntervalMinimum) } func addDrop(_ rootView: UIView) { @@ -281,15 +311,20 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID rootView.addInteraction(dropInteraction) } - public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { + public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) + -> Bool + { return true } - public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { + public func dropInteraction( + _ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession + ) -> UIDropProposal { return UIDropProposal(operation: .copy) } - public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { + public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) + { var items: [NSItemProvider] = [] session.items.forEach { item in items.append(item.itemProvider) } @@ -302,7 +337,10 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.iph?.startProcessing() } - public override func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + public override func application( + _ application: UIApplication, + performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { let fetchStart = CFAbsoluteTimeGetCurrent() NSLog("Background fetch queued...") DispatchQueue.global(qos: .default).async { @@ -318,13 +356,18 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } } - public override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + public override func application( + _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } let token = tokenParts.joined() KbSetDeviceToken(token) } - public override func application(_ application: UIApplication, didReceiveRemoteNotification notification: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + public override func application( + _ application: UIApplication, didReceiveRemoteNotification notification: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { guard let type = notification["type"] as? String else { return } if type == "chat.newmessageSilent_2" { DispatchQueue.global(qos: .default).async { @@ -341,26 +384,34 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID let pusher = PushNotifier() var err: NSError? - Keybasego.KeybaseHandleBackgroundNotification(convID, body, "", sender, membersType, displayPlaintext, messageID, pushID, badgeCount, unixTime, soundName, pusher, false, &err) + Keybasego.KeybaseHandleBackgroundNotification( + convID, body, "", sender, membersType, displayPlaintext, messageID, pushID, badgeCount, + unixTime, soundName, pusher, false, &err) if let err { NSLog("Failed to handle in engine: \(err)") } completionHandler(.newData) NSLog("Remote notification handle finished...") } } else { - var notificationDict = Dictionary(uniqueKeysWithValues: notification.map { (String(describing: $0.key), $0.value) }) + var notificationDict = Dictionary( + uniqueKeysWithValues: notification.map { (String(describing: $0.key), $0.value) }) notificationDict["userInteraction"] = false KbEmitPushNotification(notificationDict) completionHandler(.newData) } } - public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { let userInfo = response.notification.request.content.userInfo - var notificationDict = Dictionary(uniqueKeysWithValues: userInfo.map { (String(describing: $0.key), $0.value) }) + var notificationDict = Dictionary( + uniqueKeysWithValues: userInfo.map { (String(describing: $0.key), $0.value) }) notificationDict["userInteraction"] = true let type = notificationDict["type"] as? String ?? "unknown" - let convID = notificationDict["convID"] as? String ?? notificationDict["c"] as? String ?? "unknown" + let convID = + notificationDict["convID"] as? String ?? notificationDict["c"] as? String ?? "unknown" // Store the notification so it can be processed when app becomes active // This ensures navigation works even if React Native isn't ready yet @@ -371,9 +422,13 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID completionHandler() } - public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { let userInfo = notification.request.content.userInfo - var notificationDict = Dictionary(uniqueKeysWithValues: userInfo.map { (String(describing: $0.key), $0.value) }) + var notificationDict = Dictionary( + uniqueKeysWithValues: userInfo.map { (String(describing: $0.key), $0.value) }) notificationDict["userInteraction"] = false KbEmitPushNotification(notificationDict) completionHandler([]) @@ -445,7 +500,10 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID Keybasego.KeybaseFlushLogs() let elapsed = CFAbsoluteTimeGetCurrent() - AppDelegate.appStartTime let fgGap = fgEnterTime > 0 ? CFAbsoluteTimeGetCurrent() - fgEnterTime : 0 - writeStartupTimingLog(String(format: "applicationDidBecomeActive: %.1fms after launch, %.0fms after willEnterForeground", elapsed * 1000, fgGap * 1000)) + writeStartupTimingLog( + String( + format: "applicationDidBecomeActive: %.1fms after launch, %.0fms after willEnterForeground", + elapsed * 1000, fgGap * 1000)) NSLog("[Startup] applicationDidBecomeActive: %.0fms after willEnterForeground", fgGap * 1000) NSLog("applicationDidBecomeActive: hiding keyz screen.") hideCover() @@ -463,9 +521,11 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID if alreadyReEmitted { KbSetInitialNotification(storedNotification) } else { - NSLog("applicationDidBecomeActive: stored notification has userInteraction=true, emitting") + NSLog( + "applicationDidBecomeActive: stored notification has userInteraction=true, emitting") KbEmitPushNotification(storedNotification) - var copy = Dictionary(uniqueKeysWithValues: storedNotification.map { (String(describing: $0.key), $0.value) }) + var copy = Dictionary( + uniqueKeysWithValues: storedNotification.map { (String(describing: $0.key), $0.value) }) copy["reEmittedInBecomeActive"] = true KbSetInitialNotification(copy) } @@ -480,7 +540,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID public override func applicationWillEnterForeground(_ application: UIApplication) { fgEnterTime = CFAbsoluteTimeGetCurrent() let bgDuration = fgEnterTime - bgEnterWallTime - NSLog("applicationWillEnterForeground: start, hiding keyz screen (%.1fs since background)", bgDuration) + NSLog( + "applicationWillEnterForeground: start, hiding keyz screen (%.1fs since background)", + bgDuration) hideCover() NSLog("applicationWillEnterForeground: done") } @@ -494,16 +556,17 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { // Extension point for config-plugins - override func sourceURL(for bridge: RCTBridge) -> URL? { + override func bundleURL() -> URL? { // needed to return the correct URL for expo-dev-client. bridge.bundleURL ?? bundleURL() } override func bundleURL() -> URL? { -#if DEBUG - return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") -#else - return Bundle.main.url(forResource: "main", withExtension: "jsbundle") -#endif + #if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL( + forBundleRoot: ".expo/.virtual-metro-entry") + #else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif } } From 8d5bf7d648e2fe42ed2dcce75627d28e459fa74d Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:06:25 -0400 Subject: [PATCH 37/42] fix sourceURL (#29101) --- shared/ios/Keybase/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index a60f71c391af..175e89494a95 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -556,7 +556,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { // Extension point for config-plugins - override func bundleURL() -> URL? { + override func sourceURL(for bridge: RCTBridge) -> URL? { // needed to return the correct URL for expo-dev-client. bridge.bundleURL ?? bundleURL() } From 9602c11e17f934626eca6707c75fcd5cbbbc6db3 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:27:53 -0400 Subject: [PATCH 38/42] Move startup log writes off the main thread (#29103) --- shared/ios/Keybase/AppDelegate.swift | 79 +++++++++++++++------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 175e89494a95..5bdf4a3075be 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -48,7 +48,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, var fsPaths: [String: String] = [:] var shutdownTask: UIBackgroundTaskIdentifier = .invalid var iph: ItemProviderHelper? - var startupLogFileHandle: FileHandle? + private var startupLogFileHandle: FileHandle? + private let logQueue = DispatchQueue(label: "kb.startup.log", qos: .utility) private var watchdog: MainThreadWatchdog? private var bgEnterWallTime: CFAbsoluteTime = 0 @@ -154,6 +155,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, /////// KB specific private static var appStartTime: CFAbsoluteTime = 0 + private static let initLog = OSLog(subsystem: "keybase", category: "init") private static let logDateFormatter: DateFormatter = { let f = DateFormatter() @@ -167,59 +169,62 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, return } - if self.startupLogFileHandle == nil { - if !FileManager.default.fileExists(atPath: logFilePath) { - // Match the parent directory's protection class so the file remains accessible - // in the background after the device has been unlocked once. - FileManager.default.createFile( - atPath: logFilePath, contents: nil, - attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]) - } - if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { - fileHandle.seekToEndOfFile() - self.startupLogFileHandle = fileHandle - } else { - NSLog("Error opening startup timing log file: \(logFilePath)") - return - } - } - - guard let fileHandle = self.startupLogFileHandle else { - return - } - + // Capture timestamp on the calling thread so it reflects when the event actually occurred. let now = Date() let timeInterval = now.timeIntervalSince1970 let microseconds = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1_000_000) let dateString = AppDelegate.logDateFormatter.string(from: now) let timestamp = String(format: "%@.%06dZ", dateString, microseconds) - let fileName = URL(fileURLWithPath: file).lastPathComponent let logMessage = String( format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message ) - guard let logData = logMessage.data(using: .utf8) else { return } - do { - try fileHandle.write(contentsOf: logData) - try fileHandle.synchronize() - } catch { - NSLog("Error writing startup timing log: \(error)") + // Dispatch file I/O (including fsync) to a background queue so the calling thread + // (often the main thread) is never blocked by disk latency. + logQueue.async { [weak self] in + guard let self else { return } + if self.startupLogFileHandle == nil { + if !FileManager.default.fileExists(atPath: logFilePath) { + // Match the parent directory's protection class so the file remains accessible + // in the background after the device has been unlocked once. + FileManager.default.createFile( + atPath: logFilePath, contents: nil, + attributes: [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]) + } + if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { + fileHandle.seekToEndOfFile() + self.startupLogFileHandle = fileHandle + } else { + NSLog("Error opening startup timing log file: \(logFilePath)") + return + } + } + guard let fileHandle = self.startupLogFileHandle else { return } + do { + try fileHandle.write(contentsOf: logData) + try fileHandle.synchronize() + } catch { + NSLog("Error writing startup timing log: \(error)") + } } } private func closeStartupLogFile() { - if let fileHandle = self.startupLogFileHandle { - do { - try fileHandle.synchronize() - try fileHandle.close() - } catch { - NSLog("Error closing startup timing log: \(error)") + // Use sync so all pending async log writes are flushed before we close the handle. + logQueue.sync { + if let fileHandle = self.startupLogFileHandle { + do { + try fileHandle.synchronize() + try fileHandle.close() + } catch { + NSLog("Error closing startup timing log: \(error)") + } + self.startupLogFileHandle = nil } - self.startupLogFileHandle = nil } } @@ -251,7 +256,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, if let err { // Log to system log with public annotation so it's not redacted in logarchive. os_log( - .error, log: OSLog(subsystem: "keybase", category: "init"), + .error, log: AppDelegate.initLog, "KeybaseInit FAILED: %{public}@ (code=%ld domain=%{public}@)", err.localizedDescription, err.code, err.domain) // Also write to ios.log so it's captured in xcappdata even without a device attached. From 8a5af1d48e403b5a9115845834205b170ffd6e37 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:41:26 -0400 Subject: [PATCH 39/42] store init err for logging (#29104) --- rnmodules/react-native-kb/ios/Kb.h | 3 +++ rnmodules/react-native-kb/ios/Kb.mm | 10 ++++++++-- shared/ios/Keybase/AppDelegate.swift | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/rnmodules/react-native-kb/ios/Kb.h b/rnmodules/react-native-kb/ios/Kb.h index 8c919fbfa7dc..e27b59108a4d 100644 --- a/rnmodules/react-native-kb/ios/Kb.h +++ b/rnmodules/react-native-kb/ios/Kb.h @@ -24,3 +24,6 @@ FOUNDATION_EXPORT void KbSetDeviceToken(NSString *token); FOUNDATION_EXPORT void KbSetInitialNotification(NSDictionary *notification); FOUNDATION_EXPORT void KbEmitPushNotification(NSDictionary *notification); FOUNDATION_EXPORT NSDictionary *KbGetAndClearInitialNotification(void); + +// Init result - stored for inclusion in ReadArr error logs +FOUNDATION_EXPORT void KbSetInitResult(NSString *result); diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index 0673eaf9aaee..7f9805b06697 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -78,6 +78,7 @@ - (void)dealloc { static BOOL kbPasteImageEnabled = NO; static NSString *kbStoredDeviceToken = nil; static NSDictionary *kbInitialNotification = nil; +static NSString *kbInitResult = nil; @interface RCTBridge (JSIRuntime) - (void *)runtime; @@ -396,10 +397,11 @@ - (NSDictionary *)getConstants { } if (consecutiveErrors <= 5 || consecutiveErrors % 50 == 0) { __typeof__(self) strongSelf = weakSelf; - os_log(OS_LOG_DEFAULT, "ReadArr loop[%llu] error streak=%llu total=%llu domain=%{public}s code=%ld desc=%{public}s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", + os_log(OS_LOG_DEFAULT, "ReadArr loop[%llu] error streak=%llu total=%llu domain=%{public}s code=%ld desc=%{public}s initResult=%{public}s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, (unsigned long long)totalErrors, KBStringOrNil(error.domain), (long)error.code, - KBStringOrNil(error.localizedDescription), strongSelf, strongSelf.bridge, + KBStringOrNil(error.localizedDescription), KBStringOrNil(kbInitResult), + strongSelf, strongSelf.bridge, strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); } // Back off on error to avoid spinning at ~35K/sec and starving the main thread CPU @@ -752,3 +754,7 @@ void KbEmitPushNotification(NSDictionary *notification) { kbInitialNotification = nil; return notification; } + +void KbSetInitResult(NSString *result) { + kbInitResult = result; +} diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 5bdf4a3075be..7642a613be33 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -254,15 +254,17 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, securityAccessGroupOverride, nil, nil, systemVer, isIPad, nil, isIOS, shareIntentDonator, &err ) if let err { + let initResult = "FAILED: \(err.localizedDescription) (code=\(err.code) domain=\(err.domain))" + KbSetInitResult(initResult) // Log to system log with public annotation so it's not redacted in logarchive. os_log( .error, log: AppDelegate.initLog, "KeybaseInit FAILED: %{public}@ (code=%ld domain=%{public}@)", err.localizedDescription, err.code, err.domain) // Also write to ios.log so it's captured in xcappdata even without a device attached. - self.writeStartupTimingLog( - "KeybaseInit FAILED: \(err.localizedDescription) (code=\(err.code) domain=\(err.domain))") + self.writeStartupTimingLog("KeybaseInit \(initResult)") } else { + KbSetInitResult("succeeded") self.writeStartupTimingLog("KeybaseInit succeeded") } From 22f45ce8dc486be2cb268e9bd802b051407aa975 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:45:36 -0400 Subject: [PATCH 40/42] fix logfile permissions/cleanup (#29105) --- shared/ios/Keybase/Fs.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/shared/ios/Keybase/Fs.swift b/shared/ios/Keybase/Fs.swift index 7b6463e49ace..39700b52324f 100644 --- a/shared/ios/Keybase/Fs.swift +++ b/shared/ios/Keybase/Fs.swift @@ -22,11 +22,12 @@ import Foundation .appendingPathComponent("logs") .appendingPathComponent("ios.log").path + let logDirURL = oldLogURL.appendingPathComponent("logs") if !skipLogFile { - // cleanup old log files + // cleanup old log files (they live in the logs/ subdir, not directly under oldLogURL) let fm = FileManager.default ["ios.log", "ios.log.ek"].forEach { - try? fm.removeItem(at: oldLogURL.appendingPathComponent($0)) + try? fm.removeItem(at: logDirURL.appendingPathComponent($0)) } } // Create LevelDB and log directories with a slightly lower data protection @@ -45,11 +46,15 @@ import Foundation "kbfs_sync_cache", "kbfs_settings", "synced_tlf_config", - "logs" ].forEach { createBackgroundReadableDirectory(path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) } - // Mark avatars, which are in the caches dir + // Log and avatar dirs live under the caches dir, not Application Support. + // This must run after the cleanup above so that any surviving ios.log from a + // previous session (created by Go with default FileProtectionComplete) has its + // protection downgraded to completeUntilFirstUserAuthentication before + // KeybaseInit tries to open it on a locked device. + createBackgroundReadableDirectory(path: logDirURL.path, setAllFiles: true) createBackgroundReadableDirectory(path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) let setupFsElapsed = CFAbsoluteTimeGetCurrent() - setupFsStartTime From 96da9ab97a70de81bed53d30fb5aef2dcd8a90c5 Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:50:14 -0400 Subject: [PATCH 41/42] Revert unneeded startup debugging (#29107) * Revert unneeded startup debugging * x --- go/bind/keybase.go | 83 +--- .../kb-common/src/ItemProviderHelper.swift | 404 ++++++++++-------- rnmodules/kb-common/src/MediaUtils.swift | 118 +++-- rnmodules/react-native-kb/ios/Kb.mm | 89 +--- shared/ios/Keybase.xcodeproj/project.pbxproj | 4 - shared/ios/Keybase/AppDelegate.swift | 52 +-- shared/ios/Keybase/Fs.swift | 38 +- shared/ios/Keybase/MainThreadWatchdog.swift | 221 ---------- shared/ios/Keybase/Pusher.swift | 17 +- .../ios/Keybase/ShareIntentDonatorImpl.swift | 22 +- .../KeybaseShare/ShareViewController.swift | 25 +- 11 files changed, 384 insertions(+), 689 deletions(-) delete mode 100644 shared/ios/Keybase/MainThreadWatchdog.swift diff --git a/go/bind/keybase.go b/go/bind/keybase.go index f9f4695884e4..aeaf9dba40a0 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "net" "os" "path/filepath" @@ -17,7 +16,6 @@ import ( "runtime/trace" "strings" "sync" - "sync/atomic" "time" "github.com/keybase/client/go/chat/globals" @@ -61,16 +59,6 @@ var ( connMutex sync.Mutex // Protects conn operations ) -var ( - connGeneration atomic.Uint64 - resetGeneration atomic.Uint64 - jsReadySignalCount atomic.Uint64 - writeErrCount atomic.Uint64 - readErrCount atomic.Uint64 - readErrStreak atomic.Uint64 - readErrStreakStart atomic.Int64 -) - func describeConn(c net.Conn) string { if c == nil { return "" @@ -78,13 +66,6 @@ func describeConn(c net.Conn) string { return fmt.Sprintf("%T@%p", c, c) } -func describeErr(err error) string { - if err == nil { - return "" - } - return fmt.Sprintf("%T: %v", err, err) -} - func appStateForLog() string { if kbCtx == nil || kbCtx.MobileAppState == nil { return "" @@ -92,32 +73,6 @@ func appStateForLog() string { return fmt.Sprintf("%v", kbCtx.MobileAppState.State()) } -func noteReadSuccess(c net.Conn, n int) { - streak := readErrStreak.Swap(0) - startUnix := readErrStreakStart.Swap(0) - if streak == 0 { - return - } - var dur time.Duration - if startUnix > 0 { - dur = time.Since(time.Unix(0, startUnix)) - } - log("Go: ReadArr recovered after streak=%d conn=%s nextReadBytes=%d appState=%s duration=%s", - streak, describeConn(c), n, appStateForLog(), dur) -} - -func noteReadError(c net.Conn, err error) { - total := readErrCount.Add(1) - streak := readErrStreak.Add(1) - if streak == 1 { - readErrStreakStart.Store(time.Now().UnixNano()) - } - if streak <= 5 || streak == 10 || streak%50 == 0 { - log("Go: ReadArr error streak=%d total=%d conn=%s appState=%s err=%s eof=%v", - streak, total, describeConn(c), appStateForLog(), describeErr(err), errors.Is(err, io.EOF)) - } -} - // log writes to kbCtx.Log if available, otherwise falls back to stderr. // Stderr is captured in crash logs and the Xcode console, making early Init // messages (before kbCtx.Log is set up by Configure) visible in diagnostics. @@ -530,9 +485,8 @@ func WriteArr(b []byte) (err error) { n, err := currentConn.Write(bytes) if err != nil { - total := writeErrCount.Add(1) - log("Go: WriteArr error total=%d conn=%s len=%d appState=%s err=%s", - total, describeConn(currentConn), len(bytes), appStateForLog(), describeErr(err)) + log("Go: WriteArr error conn=%s len=%d appState=%s err=%v", + describeConn(currentConn), len(bytes), appStateForLog(), err) return fmt.Errorf("Write error: %s", err) } if n != len(bytes) { @@ -573,12 +527,10 @@ func ReadArr() (data []byte, err error) { n, err := currentConn.Read(buffer) if n > 0 && err == nil { - noteReadSuccess(currentConn, n) return buffer[0:n], nil } if err != nil { - noteReadError(currentConn, err) // Attempt to fix the connection if ierr := Reset(); ierr != nil { log("failed to Reset: %v", ierr) @@ -592,7 +544,6 @@ func ReadArr() (data []byte, err error) { // ensureConnection establishes the loopback connection if not already connected. // Must be called with connMutex held. func ensureConnection() error { - start := time.Now() if !isInited() { log("ensureConnection: keybase not initialized") return errors.New("keybase not initialized") @@ -603,8 +554,6 @@ func ensureConnection() error { } var err error - log("ensureConnection: attempting dial listener=%T@%p existingConn=%s appState=%s", - kbCtx.LoopbackListener, kbCtx.LoopbackListener, describeConn(conn), appStateForLog()) conn, err = kbCtx.LoopbackListener.Dial() if err != nil { // The listener was closed (isClosed=true, returns syscall.EINVAL). Recreate it and @@ -621,14 +570,12 @@ func ensureConnection() error { log("ensureConnection: Dial failed after restart: %v", err) return fmt.Errorf("failed to dial after loopback restart: %s", err) } - gen := connGeneration.Add(1) - log("ensureConnection: loopback server restarted successfully in %v gen=%d conn=%s appState=%s", - time.Since(start), gen, describeConn(conn), appStateForLog()) + log("ensureConnection: loopback server restarted successfully conn=%s appState=%s", + describeConn(conn), appStateForLog()) return nil } - gen := connGeneration.Add(1) - log("Go: Established loopback connection in %v gen=%d conn=%s appState=%s", - time.Since(start), gen, describeConn(conn), appStateForLog()) + log("Go: Established loopback connection conn=%s appState=%s", + describeConn(conn), appStateForLog()) return nil } @@ -637,19 +584,18 @@ func Reset() error { connMutex.Lock() defer connMutex.Unlock() - resetID := resetGeneration.Add(1) - log("Go: Reset #%d start conn=%s appState=%s", resetID, describeConn(conn), appStateForLog()) + log("Go: Reset start conn=%s appState=%s", describeConn(conn), appStateForLog()) if conn != nil { conn.Close() conn = nil } if kbCtx == nil || kbCtx.LoopbackListener == nil { - log("Go: Reset #%d complete without listener appState=%s", resetID, appStateForLog()) + log("Go: Reset complete without listener appState=%s", appStateForLog()) return nil } // Connection will be re-established lazily on next read/write - log("Go: Connection reset, will reconnect on next operation (reset=%d appState=%s)", resetID, appStateForLog()) + log("Go: Connection reset, will reconnect on next operation appState=%s", appStateForLog()) return nil } @@ -657,18 +603,11 @@ func Reset() error { // This unblocks the ReadArr loop and allows bidirectional communication. // jsReadyCh is closed once and stays closed — repeated calls from engine resets are no-ops. func NotifyJSReady() { - call := jsReadySignalCount.Add(1) - notified := false jsReadyOnce.Do(func() { - notified = true - log("Go: JS signaled ready, unblocking RPC communication (call=%d appState=%s conn=%s)", - call, appStateForLog(), describeConn(conn)) + log("Go: JS signaled ready, unblocking RPC communication appState=%s conn=%s", + appStateForLog(), describeConn(conn)) close(jsReadyCh) }) - if !notified { - log("Go: NotifyJSReady called again (no-op, channel already closed — engine reset?) call=%d appState=%s conn=%s", - call, appStateForLog(), describeConn(conn)) - } } // ForceGC Forces a gc diff --git a/rnmodules/kb-common/src/ItemProviderHelper.swift b/rnmodules/kb-common/src/ItemProviderHelper.swift index 41273e67ffbc..3dca9745c613 100644 --- a/rnmodules/kb-common/src/ItemProviderHelper.swift +++ b/rnmodules/kb-common/src/ItemProviderHelper.swift @@ -4,7 +4,6 @@ import Foundation import UIKit import UniformTypeIdentifiers - @objc(ItemProviderHelper) public class ItemProviderHelper: NSObject { private var itemArrs: [Any] @@ -14,12 +13,12 @@ public class ItemProviderHelper: NSObject { private var typeToArray: [String: [[String: Any]]] = [:] private var completionHandler: () -> Void private var unprocessed: Int = 0 - + @objc public var manifest: [[String: Any]] { // reconcile what we're sending over. types=text, url, video, image, file, error var toWrite: [[String: Any]] = [] let urls = typeToArray["url"] - + // We treat all text that has http in it a url, we take the longest one as its // likely most descriptive if let urls = urls { @@ -31,14 +30,14 @@ public class ItemProviderHelper: NSObject { } toWrite.append([ "type": "text", - "content": content + "content": content, ]) } else { let images = typeToArray["image"] ?? [] let videos = typeToArray["video"] ?? [] let files = typeToArray["file"] ?? [] let texts = typeToArray["text"] ?? [] - + // If we have media, ignore text, we want to attach stuff and not also // inject into the input box if !images.isEmpty || !videos.isEmpty || !files.isEmpty { @@ -50,17 +49,19 @@ public class ItemProviderHelper: NSObject { toWrite.append(texts[0]) } } - + return toWrite } - - @objc public init(forShare isShare: Bool, withItems itemArrs: [Any], completionHandler: @escaping () -> Void) { + + @objc public init( + forShare isShare: Bool, withItems itemArrs: [Any], completionHandler: @escaping () -> Void + ) { self.isShare = isShare self.itemArrs = itemArrs self.completionHandler = completionHandler self.payloadFolderURL = Self.makePayloadFolder() } - + private func completeProcessingItemAlreadyInMainThread() { // more to process objc_sync_enter(self) @@ -70,7 +71,7 @@ public class ItemProviderHelper: NSObject { return } objc_sync_exit(self) - + // done if !done { done = true @@ -80,76 +81,82 @@ public class ItemProviderHelper: NSObject { // already done? } } - + private static func getIncomingShareFolder() -> URL { - let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.keybase")! + let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.keybase")! // Use the cache URL so if we fail to clean up payloads they can be deleted by // the OS. let cacheURL = containerURL.appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true) - let incomingShareFolderURL = cacheURL.appendingPathComponent("incoming-shares", isDirectory: true) + let incomingShareFolderURL = cacheURL.appendingPathComponent( + "incoming-shares", isDirectory: true) return incomingShareFolderURL } - + private static func makePayloadFolder() -> URL { let incomingShareFolderURL = getIncomingShareFolder() let guid = ProcessInfo.processInfo.globallyUniqueString let payloadFolderURL = incomingShareFolderURL.appendingPathComponent(guid, isDirectory: true) - try? FileManager.default.createDirectory(at: payloadFolderURL, withIntermediateDirectories: true, attributes: nil) + try? FileManager.default.createDirectory( + at: payloadFolderURL, withIntermediateDirectories: true, attributes: nil) return payloadFolderURL } - + private func getPayloadURL(from url: URL?) -> URL { let guid = ProcessInfo.processInfo.globallyUniqueString - return url != nil ? payloadFolderURL.appendingPathComponent(url!.lastPathComponent) - : payloadFolderURL.appendingPathComponent(guid) + return url != nil + ? payloadFolderURL.appendingPathComponent(url!.lastPathComponent) + : payloadFolderURL.appendingPathComponent(guid) } - + private func getPayloadURL(withExtension ext: String?) -> URL { let guid = ProcessInfo.processInfo.globallyUniqueString - return ext != nil ? payloadFolderURL.appendingPathComponent(guid).appendingPathExtension(ext!) - : payloadFolderURL.appendingPathComponent(guid) + return ext != nil + ? payloadFolderURL.appendingPathComponent(guid).appendingPathExtension(ext!) + : payloadFolderURL.appendingPathComponent(guid) } - + private func getManifestFileURL() -> URL { let incomingShareFolderURL = Self.getIncomingShareFolder() - try? FileManager.default.createDirectory(at: incomingShareFolderURL, withIntermediateDirectories: true, attributes: nil) + try? FileManager.default.createDirectory( + at: incomingShareFolderURL, withIntermediateDirectories: true, attributes: nil) return incomingShareFolderURL.appendingPathComponent("manifest.json") } - + private func ensureArray(ofType type: String) -> [[String: Any]] { if typeToArray[type] == nil { typeToArray[type] = [] } return typeToArray[type]! } - + private func completeItemAndAppendManifest(type: String, originalFileURL: URL) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } var arr = self.ensureArray(ofType: type) arr.append([ "type": type, - "originalPath": originalFileURL.absoluteURL.path + "originalPath": originalFileURL.absoluteURL.path, ]) self.typeToArray[type] = arr self.completeProcessingItemAlreadyInMainThread() } } - + private func completeItemAndAppendManifest(type: String, content: String) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } var arr = self.ensureArray(ofType: type) arr.append([ "type": type, - "content": content + "content": content, ]) self.typeToArray[type] = arr self.completeProcessingItemAlreadyInMainThread() } } - + private func completeItemAndAppendManifest(type: String, originalFileURL: URL, content: String) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -157,27 +164,28 @@ public class ItemProviderHelper: NSObject { arr.append([ "type": type, "originalPath": originalFileURL.absoluteURL.path, - "content": content + "content": content, ]) self.typeToArray[type] = arr self.completeProcessingItemAlreadyInMainThread() } } - - private func completeItemAndAppendManifest(type: String, originalFileURL: URL, scaledFileURL: URL) { + + private func completeItemAndAppendManifest(type: String, originalFileURL: URL, scaledFileURL: URL) + { DispatchQueue.main.async { [weak self] in guard let self = self else { return } var arr = self.ensureArray(ofType: type) arr.append([ "type": type, "originalPath": originalFileURL.absoluteURL.path, - "scaledPath": scaledFileURL.absoluteURL.path + "scaledPath": scaledFileURL.absoluteURL.path, ]) self.typeToArray[type] = arr self.completeProcessingItemAlreadyInMainThread() } } - + private func completeItemAndAppendManifestAndLogError(text: String, error: Error?) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -189,7 +197,7 @@ public class ItemProviderHelper: NSObject { self.completeProcessingItemAlreadyInMainThread() } } - + private func writeManifest() { let toWrite = manifest let fileURL = getManifestFileURL() @@ -200,58 +208,63 @@ public class ItemProviderHelper: NSObject { JSONSerialization.writeJSONObject(toWrite, to: outputStream, options: [], error: nil) } } - + private func handleAndCompleteMediaFile(_ url: URL, isVideo: Bool) { if isVideo { MediaUtils.processVideo(fromOriginal: url) { error, scaled in if let error = error { - self.completeItemAndAppendManifestAndLogError(text: "handleAndCompleteMediaFile", error: error) + self.completeItemAndAppendManifestAndLogError( + text: "handleAndCompleteMediaFile", error: error) return } - self.completeItemAndAppendManifest(type: isVideo ? "video" : "image", - originalFileURL: url, - scaledFileURL: scaled!) + self.completeItemAndAppendManifest( + type: isVideo ? "video" : "image", + originalFileURL: url, + scaledFileURL: scaled!) } } else { MediaUtils.processImage(fromOriginal: url) { error, scaled in if let error = error { - self.completeItemAndAppendManifestAndLogError(text: "handleAndCompleteMediaFile", error: error) + self.completeItemAndAppendManifestAndLogError( + text: "handleAndCompleteMediaFile", error: error) return } - self.completeItemAndAppendManifest(type: isVideo ? "video" : "image", - originalFileURL: url, - scaledFileURL: scaled!) + self.completeItemAndAppendManifest( + type: isVideo ? "video" : "image", + originalFileURL: url, + scaledFileURL: scaled!) } } } - + private func sendText(_ text: String) { if text.isEmpty { completeItemAndAppendManifestAndLogError(text: "sendText: empty?", error: nil) return } - + if text.count < 1000 { let isURL = text.range(of: "http", options: .caseInsensitive) != nil completeItemAndAppendManifest(type: isURL ? "url" : "text", content: text) return } - + let originalFileURL = getPayloadURL(withExtension: "txt") do { try text.write(to: originalFileURL, atomically: true, encoding: .utf8) completeItemAndAppendManifest(type: "text", originalFileURL: originalFileURL) } catch { - completeItemAndAppendManifestAndLogError(text: "sendText: unable to write payload file", error: error) + completeItemAndAppendManifestAndLogError( + text: "sendText: unable to write payload file", error: error) } } - + private func sendFile(_ url: URL?) { guard let url = url else { completeItemAndAppendManifestAndLogError(text: "sendFile: unable to decode share", error: nil) return } - + let filePayloadURL = getPayloadURL(from: url) do { try FileManager.default.copyItem(at: url, to: filePayloadURL) @@ -260,45 +273,47 @@ public class ItemProviderHelper: NSObject { completeItemAndAppendManifestAndLogError(text: "fileHandlerSimple: copy error", error: error) } } - + private func sendContact(_ vCardData: Data) { do { let contacts = try CNContactVCardSerialization.contacts(with: vCardData) let addressFormatter = CNPostalAddressFormatter() var contents: [String] = [] - + for contact in contacts { var content: [String] = [] - if let fullName = CNContactFormatter.string(from: contact, style: .fullName), !fullName.isEmpty { + if let fullName = CNContactFormatter.string(from: contact, style: .fullName), + !fullName.isEmpty + { content.append(fullName) } - + // For NSString properties, we need to check length if (contact.organizationName as NSString).length > 0 { content.append("Organization: \(contact.organizationName)") } - + for phoneNumber in contact.phoneNumbers { let label = CNLabeledValue.localizedString(forLabel: phoneNumber.label ?? "") let number = phoneNumber.value.stringValue - + if (label as NSString).length > 0 && (number as NSString).length > 0 { content.append("\(label): \(number)") } else if (number as NSString).length > 0 { content.append(number) } } - + // Handle email addresses and URLs var misc: [Any] = [] misc.append(contentsOf: contact.emailAddresses) misc.append(contentsOf: contact.urlAddresses) - + for m in misc { if let labeledValue = m as? CNLabeledValue { let label = CNLabeledValue.localizedString(forLabel: labeledValue.label ?? "") let val = labeledValue.value as String - + if !label.isEmpty && !val.isEmpty { content.append("\(label): \(val)") } else if !val.isEmpty { @@ -306,39 +321,41 @@ public class ItemProviderHelper: NSObject { } } } - + // Handle postal addresses for postalAddress in contact.postalAddresses { let label = CNLabeledValue.localizedString(forLabel: postalAddress.label ?? "") let val = addressFormatter.string(from: postalAddress.value) - + if !label.isEmpty && !val.isEmpty { content.append("\(label): \(val)") } else if !val.isEmpty { content.append(val) } } - + if !content.isEmpty { contents.append(content.joined(separator: "\n")) } } - + if !contents.isEmpty { let text = contents.joined(separator: "\n\n") completeItemAndAppendManifest(type: "text", content: text) } else { - completeItemAndAppendManifestAndLogError(text: "vcardHandler: unable to decode share", error: nil) + completeItemAndAppendManifestAndLogError( + text: "vcardHandler: unable to decode share", error: nil) } } catch { - completeItemAndAppendManifestAndLogError(text: "vcardHandler: error processing vcard", error: error) + completeItemAndAppendManifestAndLogError( + text: "vcardHandler: error processing vcard", error: error) } } - + private func sendMovie(_ url: URL?) { var filePayloadURL: URL? var error: Error? - + if let url = url { filePayloadURL = getPayloadURL(from: url) do { @@ -347,14 +364,15 @@ public class ItemProviderHelper: NSObject { error = copyError } } - + if let filePayloadURL = filePayloadURL, error == nil { handleAndCompleteMediaFile(filePayloadURL, isVideo: true) } else { - completeItemAndAppendManifestAndLogError(text: "movieFileHandlerSimple2: copy error", error: error) + completeItemAndAppendManifestAndLogError( + text: "movieFileHandlerSimple2: copy error", error: error) } } - + private func sendImage(_ imgData: Data?) { if let imgData = imgData { let originalFileURL = getPayloadURL(withExtension: "jpg") @@ -364,152 +382,162 @@ public class ItemProviderHelper: NSObject { return } } - - completeItemAndAppendManifestAndLogError(text: "coerceImageHandlerSimple2: unable to decode share", error: nil) + + completeItemAndAppendManifestAndLogError( + text: "coerceImageHandlerSimple2: unable to decode share", error: nil) } - + private func incrementUnprocessed() { objc_sync_enter(self) unprocessed += 1 objc_sync_exit(self) } - + @objc public func startProcessing() { // Handlers for different types let fileHandler: @Sendable (URL?, Error?) -> Void = { url, error in - self.sendFile(url) + self.sendFile(url) } - + let movieHandler: @Sendable (URL?, Error?) -> Void = { url, error in - self.sendMovie(error == nil ? url : nil) + self.sendMovie(error == nil ? url : nil) } - + let imageHandler: @Sendable (NSSecureCoding?, Error?) -> Void = { item, error in - var imgData: Data? // Changed to var so we can modify it - if error == nil { - if let url = item as? URL { - imgData = try? Data(contentsOf: url) - if let data = imgData { - let image = UIImage(data: data) - imgData = image?.jpegData(compressionQuality: 0.85) - } - } else if let image = item as? UIImage { - imgData = image.jpegData(compressionQuality: 0.85) - } + var imgData: Data? // Changed to var so we can modify it + if error == nil { + if let url = item as? URL { + imgData = try? Data(contentsOf: url) + if let data = imgData { + let image = UIImage(data: data) + imgData = image?.jpegData(compressionQuality: 0.85) + } + } else if let image = item as? UIImage { + imgData = image.jpegData(compressionQuality: 0.85) } - self.sendImage(imgData) + } + self.sendImage(imgData) } - + let contactHandler: @Sendable (Data?, Error?) -> Void = { data, error in - if let data = data { - self.sendContact(data) - } else { - self.completeItemAndAppendManifestAndLogError( - text: "vcardHandler: unable to decode share", - error: nil - ) - } + if let data = data { + self.sendContact(data) + } else { + self.completeItemAndAppendManifestAndLogError( + text: "vcardHandler: unable to decode share", + error: nil + ) + } } - + let secureTextHandler: @Sendable (NSSecureCoding?, Error?) -> Void = { item, error in - var text: String? // Changed to var so we can modify it - if error == nil { - if let str = item as? String { - text = str - } else if let url = item as? URL { - text = url.absoluteString - if text?.hasPrefix("file://") == true { - let d = try? Data(contentsOf: url) - text = d != nil ? String(data: d!, encoding: .utf8) : nil - } - } else if let data = item as? Data { - text = String(data: data, encoding: .utf8) - } + var text: String? // Changed to var so we can modify it + if error == nil { + if let str = item as? String { + text = str + } else if let url = item as? URL { + text = url.absoluteString + if text?.hasPrefix("file://") == true { + let d = try? Data(contentsOf: url) + text = d != nil ? String(data: d!, encoding: .utf8) : nil + } + } else if let data = item as? Data { + text = String(data: data, encoding: .utf8) } - self.sendText(text ?? "") + } + self.sendText(text ?? "") } // Rest of the method remains the same... for items in itemArrs as! [[NSItemProvider]] { - // Only handle one from itemArrs if we're already processing - objc_sync_enter(self) - if unprocessed > 0 { - objc_sync_exit(self) - break - } + // Only handle one from itemArrs if we're already processing + objc_sync_enter(self) + if unprocessed > 0 { objc_sync_exit(self) - - for item in items { - for stype in item.registeredTypeIdentifiers { - guard let type = UTType(stype) else { continue } - - // Movies - if type.conforms(to: .movie) { - incrementUnprocessed() - item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: movieHandler) - break - } - // Images (PNG, GIF, JPEG) - else if type.conforms(to: .png) || type.conforms(to: .gif) || type.conforms(to: .jpeg) { - incrementUnprocessed() - item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: fileHandler) - break - } - // HEIC Images - else if stype == "public.heic" { - incrementUnprocessed() - item.loadFileRepresentation(forTypeIdentifier: "public.heic", completionHandler: fileHandler) - break - } - // Other Images (coerce) - else if type.conforms(to: .image) { - incrementUnprocessed() - item.loadItem(forTypeIdentifier: "public.image", options: nil, completionHandler: imageHandler) - break - } - // Contact cards - else if type.conforms(to: .vCard) { - incrementUnprocessed() - item.loadDataRepresentation(forTypeIdentifier: "public.vcard", completionHandler: contactHandler) - break - } - // Plain Text (two attempts - direct and coerced) - else if type.conforms(to: .plainText) { - incrementUnprocessed() - item.loadItem(forTypeIdentifier: "public.plain-text", options: nil, completionHandler: { (item: NSSecureCoding?, error: Error?) in - let text = item as? String ?? "" - self.sendText(text) - }) - - - incrementUnprocessed() - item.loadItem(forTypeIdentifier: "public.plain-text", options: nil, completionHandler: secureTextHandler) - break - } - // Files (PDF, generic files) - else if type.conforms(to: .pdf) || type.conforms(to: .fileURL) { - incrementUnprocessed() - item.loadFileRepresentation(forTypeIdentifier: "public.item", completionHandler: fileHandler) - break - } - // URLs - else if type.conforms(to: .url) { - incrementUnprocessed() - item.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { (item: NSSecureCoding?, error: Error?) in - let url = item as? URL - self.sendText(url?.absoluteString ?? "") - }) - - break - } - } + break + } + objc_sync_exit(self) + + for item in items { + for stype in item.registeredTypeIdentifiers { + guard let type = UTType(stype) else { continue } + + // Movies + if type.conforms(to: .movie) { + incrementUnprocessed() + item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: movieHandler) + break + } + // Images (PNG, GIF, JPEG) + else if type.conforms(to: .png) || type.conforms(to: .gif) || type.conforms(to: .jpeg) { + incrementUnprocessed() + item.loadFileRepresentation(forTypeIdentifier: stype, completionHandler: fileHandler) + break + } + // HEIC Images + else if stype == "public.heic" { + incrementUnprocessed() + item.loadFileRepresentation( + forTypeIdentifier: "public.heic", completionHandler: fileHandler) + break + } + // Other Images (coerce) + else if type.conforms(to: .image) { + incrementUnprocessed() + item.loadItem( + forTypeIdentifier: "public.image", options: nil, completionHandler: imageHandler) + break + } + // Contact cards + else if type.conforms(to: .vCard) { + incrementUnprocessed() + item.loadDataRepresentation( + forTypeIdentifier: "public.vcard", completionHandler: contactHandler) + break + } + // Plain Text (two attempts - direct and coerced) + else if type.conforms(to: .plainText) { + incrementUnprocessed() + item.loadItem( + forTypeIdentifier: "public.plain-text", options: nil, + completionHandler: { (item: NSSecureCoding?, error: Error?) in + let text = item as? String ?? "" + self.sendText(text) + }) + + incrementUnprocessed() + item.loadItem( + forTypeIdentifier: "public.plain-text", options: nil, + completionHandler: secureTextHandler) + break + } + // Files (PDF, generic files) + else if type.conforms(to: .pdf) || type.conforms(to: .fileURL) { + incrementUnprocessed() + item.loadFileRepresentation( + forTypeIdentifier: "public.item", completionHandler: fileHandler) + break + } + // URLs + else if type.conforms(to: .url) { + incrementUnprocessed() + item.loadItem( + forTypeIdentifier: "public.url", options: nil, + completionHandler: { (item: NSSecureCoding?, error: Error?) in + let url = item as? URL + self.sendText(url?.absoluteString ?? "") + }) + + break + } } + } } - + incrementUnprocessed() // Clean up if we didn't find anything DispatchQueue.main.async { [weak self] in - self?.completeProcessingItemAlreadyInMainThread() + self?.completeProcessingItemAlreadyInMainThread() } -} + } } diff --git a/rnmodules/kb-common/src/MediaUtils.swift b/rnmodules/kb-common/src/MediaUtils.swift index f57b1aac0744..0ef020f9d807 100644 --- a/rnmodules/kb-common/src/MediaUtils.swift +++ b/rnmodules/kb-common/src/MediaUtils.swift @@ -1,7 +1,7 @@ -import Foundation import AVFoundation -import UIKit +import Foundation import ImageIO +import UIKit struct MediaProcessingConfig { static let imageMaxPixelSize: Int = 1200 @@ -35,12 +35,14 @@ class MediaUtils: NSObject { return [ kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailFromImageAlways: true, - kCGImageSourceThumbnailMaxPixelSize: MediaProcessingConfig.imageMaxPixelSize + kCGImageSourceThumbnailMaxPixelSize: MediaProcessingConfig.imageMaxPixelSize, ] as CFDictionary } - @objc static func processImage(fromOriginal url: URL, - completion: @escaping (Error?, URL?) -> Void) { + @objc static func processImage( + fromOriginal url: URL, + completion: @escaping (Error?, URL?) -> Void + ) { processImageAsync(fromOriginal: url) { result in switch result { case .success(let url): @@ -51,8 +53,10 @@ class MediaUtils: NSObject { } } - static func processImageAsync(fromOriginal url: URL, - completion: @escaping ProcessMediaCompletion) { + static func processImageAsync( + fromOriginal url: URL, + completion: @escaping ProcessMediaCompletion + ) { DispatchQueue.global(qos: .userInitiated).async { do { let processedURL = try processImageSync(fromOriginal: url) @@ -89,8 +93,10 @@ class MediaUtils: NSObject { return scaledURL } - @objc static func processVideo(fromOriginal url: URL, - completion: @escaping (Error?, URL?) -> Void) { + @objc static func processVideo( + fromOriginal url: URL, + completion: @escaping (Error?, URL?) -> Void + ) { processVideoAsync(fromOriginal: url) { result in switch result { case .success(let url): @@ -101,9 +107,11 @@ class MediaUtils: NSObject { } } - static func processVideoAsync(fromOriginal url: URL, - progress: ProcessMediaProgressCallback? = nil, - completion: @escaping ProcessMediaCompletion) { + static func processVideoAsync( + fromOriginal url: URL, + progress: ProcessMediaProgressCallback? = nil, + completion: @escaping ProcessMediaCompletion + ) { DispatchQueue.global(qos: .userInitiated).async { do { let processedURL = try processVideoSync(fromOriginal: url, progress: progress) @@ -118,8 +126,10 @@ class MediaUtils: NSObject { } } - private static func processVideoSync(fromOriginal url: URL, - progress: ProcessMediaProgressCallback? = nil) throws -> URL { + private static func processVideoSync( + fromOriginal url: URL, + progress: ProcessMediaProgressCallback? = nil + ) throws -> URL { guard FileManager.default.fileExists(atPath: url.path) else { throw MediaUtilsError.invalidInput("File does not exist at path: \(url.path)") } @@ -134,10 +144,11 @@ class MediaUtils: NSObject { let exportSettings = determineOptimalExportSettings(for: asset) - try exportVideoWithSettings(asset: asset, - outputURL: processedURL, - settings: exportSettings, - progress: progress) + try exportVideoWithSettings( + asset: asset, + outputURL: processedURL, + settings: exportSettings, + progress: progress) return processedURL } @@ -156,13 +167,16 @@ class MediaUtils: NSObject { generator.appliesPreferredTrackTransform = true do { - _ = try generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) + _ = try generator.copyCGImage( + at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) } catch { - throw MediaUtilsError.videoProcessingFailed("Failed to generate video thumbnail: \(error.localizedDescription)") + throw MediaUtilsError.videoProcessingFailed( + "Failed to generate video thumbnail: \(error.localizedDescription)") } } - private static func determineOptimalExportSettings(for asset: AVURLAsset) -> VideoExportSettings { + private static func determineOptimalExportSettings(for asset: AVURLAsset) -> VideoExportSettings + { let videoTracks = asset.tracks(withMediaType: .video) guard let firstVideoTrack = videoTracks.first else { return VideoExportSettings.default @@ -173,8 +187,9 @@ class MediaUtils: NSObject { let fileSize = getFileSize(for: asset.url) // Determine if we need to scale down - let needsScaling = pixelCount > MediaProcessingConfig.videoMaxPixels || - fileSize > MediaProcessingConfig.videoMaxFileSize + let needsScaling = + pixelCount > MediaProcessingConfig.videoMaxPixels + || fileSize > MediaProcessingConfig.videoMaxFileSize if needsScaling { return VideoExportSettings.mediumQuality @@ -183,22 +198,25 @@ class MediaUtils: NSObject { } } - private static func exportVideoWithSettings(asset: AVURLAsset, - outputURL: URL, - settings: VideoExportSettings, - progress: ProcessMediaProgressCallback?) throws { + private static func exportVideoWithSettings( + asset: AVURLAsset, + outputURL: URL, + settings: VideoExportSettings, + progress: ProcessMediaProgressCallback? + ) throws { let semaphore = DispatchSemaphore(value: 0) var exportError: Error? - guard let exportSession = AVAssetExportSession(asset: asset, presetName: settings.preset) else { + guard let exportSession = AVAssetExportSession(asset: asset, presetName: settings.preset) + else { throw MediaUtilsError.videoProcessingFailed("Failed to create export session") } exportSession.outputURL = outputURL exportSession.outputFileType = .mp4 exportSession.shouldOptimizeForNetworkUse = true - exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing() // Strips location data + exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing() // Strips location data // Set up progress monitoring if let progress = progress { @@ -223,11 +241,13 @@ class MediaUtils: NSObject { semaphore.wait() if let error = exportError { - throw MediaUtilsError.videoProcessingFailed("Export failed: \(error.localizedDescription)") + throw MediaUtilsError.videoProcessingFailed( + "Export failed: \(error.localizedDescription)") } guard exportSession.status == .completed else { - throw MediaUtilsError.videoProcessingFailed("Export session failed with status: \(exportSession.status)") + throw MediaUtilsError.videoProcessingFailed( + "Export session failed with status: \(exportSession.status)") } } @@ -240,19 +260,25 @@ class MediaUtils: NSObject { } } - private static func scaleDownCGImageSource(_ img: CGImageSource, dstURL: URL, options: CFDictionary) throws { + private static func scaleDownCGImageSource( + _ img: CGImageSource, dstURL: URL, options: CFDictionary + ) throws { guard let scaledRef = CGImageSourceCreateThumbnailAtIndex(img, 0, options) else { throw MediaUtilsError.imageProcessingFailed("Failed to create thumbnail") } - guard let scaled = UIImage(cgImage: scaledRef).jpegData(compressionQuality: MediaProcessingConfig.imageCompressionQuality) else { + guard + let scaled = UIImage(cgImage: scaledRef).jpegData( + compressionQuality: MediaProcessingConfig.imageCompressionQuality) + else { throw MediaUtilsError.imageProcessingFailed("Failed to create JPEG data") } do { try scaled.write(to: dstURL) } catch { - throw MediaUtilsError.fileOperationFailed("Failed to write scaled image: \(error.localizedDescription)") + throw MediaUtilsError.fileOperationFailed( + "Failed to write scaled image: \(error.localizedDescription)") } } @@ -270,17 +296,22 @@ class MediaUtils: NSObject { try? FileManager.default.removeItem(at: tmpDstURL) } - guard let cgDestination = CGImageDestinationCreateWithURL(tmpDstURL as CFURL, type!, count, nil) else { + guard + let cgDestination = CGImageDestinationCreateWithURL( + tmpDstURL as CFURL, type!, count, nil) + else { throw MediaUtilsError.imageProcessingFailed("Failed to create image destination") } - let removeExifProperties = [ - kCGImagePropertyExifDictionary: kCFNull, - kCGImagePropertyGPSDictionary: kCFNull - ] as CFDictionary + let removeExifProperties = + [ + kCGImagePropertyExifDictionary: kCFNull, + kCGImagePropertyGPSDictionary: kCFNull, + ] as CFDictionary for index in 0.. #import #import -#import #import -#include #import #import #import @@ -26,14 +24,12 @@ using namespace std; using namespace kb; -extern void *currentRuntime; - // used to keep track of objects getting destroyed on the js side class KBTearDown : public jsi::HostObject { public: KBTearDown() { Tearup(); } virtual ~KBTearDown() { - NSLog(@"KBTeardown!!! currentRuntime=%p", currentRuntime); + NSLog(@"KBTeardown!!!"); Teardown(); } virtual jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) { @@ -104,16 +100,6 @@ @implementation Kb jsi::Runtime *_jsRuntime; std::shared_ptr jsScheduler; -// sanity check the runtime isn't out of sync due to reload etc -void *currentRuntime = nil; -std::atomic gKbInstallCount{0}; -std::atomic gNotifyJSReadyCount{0}; -std::atomic gReadLoopGeneration{0}; - -static const char *KBStringOrNil(NSString *value) { - return value ? value.UTF8String : ""; -} - RCT_EXPORT_MODULE() + (BOOL)requiresMainQueueSetup { @@ -172,10 +158,8 @@ + (void)handlePastedImages:(NSArray *)images { } - (void)invalidate { - NSLog(@"Kb.invalidate: self=%p bridge=%p readQueue=%p jsRuntime=%p currentRuntime=%p scheduler=%p", - self, self.bridge, self.readQueue, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); + NSLog(@"Kb.invalidate"); [[NSNotificationCenter defaultCenter] removeObserver:self]; - currentRuntime = nil; _jsRuntime = nil; kbPasteImageEnabled = NO; [super invalidate]; @@ -214,15 +198,6 @@ - (void)sendToJS:(NSData *)data { NSLog(@"Failed to find jsi in sendToJS invokeAsync!!!"); return; } - if (currentRuntime && currentRuntime != jsRuntimePtr) { - NSLog(@"sendToJS: stored runtime mismatch self=%p bridge=%p storedRuntime=%p currentRuntime=%p callbackRuntime=%p", - strongSelf, strongSelf.bridge, jsRuntimePtr, currentRuntime, &jsiRuntime); - } - if (currentRuntime && currentRuntime != &jsiRuntime) { - NSLog(@"sendToJS: callback runtime mismatch self=%p bridge=%p storedRuntime=%p currentRuntime=%p callbackRuntime=%p", - strongSelf, strongSelf.bridge, jsRuntimePtr, currentRuntime, &jsiRuntime); - } - int size = (int)[data length]; if (size <= 0) { NSLog(@"Invalid data size in sendToJS: %d", size); @@ -333,8 +308,7 @@ - (NSDictionary *)getConstants { } RCT_EXPORT_METHOD(engineReset) { - NSLog(@"engineReset: called (JS hot reload), resetting Go engine self=%p bridge=%p jsRuntime=%p currentRuntime=%p scheduler=%p", - self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); + NSLog(@"engineReset: called (JS hot reload), resetting Go engine"); NSError *error = nil; KeybaseReset(&error); [self sendEventWithName:metaEventName body:metaEventEngineReset]; @@ -345,15 +319,9 @@ - (NSDictionary *)getConstants { RCT_EXPORT_METHOD(notifyJSReady) { __weak __typeof__(self) weakSelf = self; - uint64_t notifyCount = gNotifyJSReadyCount.fetch_add(1) + 1; - NSLog(@"notifyJSReady[%llu]: called from JS, queuing main thread block self=%p bridge=%p jsRuntime=%p currentRuntime=%p scheduler=%p", - (unsigned long long)notifyCount, self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, jsScheduler.get()); + NSLog(@"notifyJSReady: called from JS self=%p bridge=%p", self, self.bridge); dispatch_async(dispatch_get_main_queue(), ^{ - uint64_t readLoopGen = gReadLoopGeneration.fetch_add(1) + 1; - NSLog(@"notifyJSReady[%llu]: main thread block executing self=%p bridge=%p jsRuntime=%p currentRuntime=%p nextReadLoop=%llu", - (unsigned long long)notifyCount, self, self.bridge, [self javaScriptRuntimePointer], currentRuntime, - (unsigned long long)readLoopGen); // Setup infrastructure [[NSNotificationCenter defaultCenter] addObserver:self @@ -364,25 +332,15 @@ - (NSDictionary *)getConstants { // Signal to Go that JS is ready KeybaseNotifyJSReady(); - NSLog(@"notifyJSReady[%llu]: Notified Go that JS is ready, starting ReadArr loop=%llu queue=%p", - (unsigned long long)notifyCount, (unsigned long long)readLoopGen, self.readQueue); + NSLog(@"notifyJSReady: Notified Go that JS is ready, starting ReadArr loop"); // Start the read loop dispatch_async(self.readQueue, ^{ - uint64_t consecutiveErrors = 0; - uint64_t totalErrors = 0; - CFAbsoluteTime errorStreakStart = 0; - NSLog(@"ReadArr loop[%llu] start self=%p bridge=%p readQueue=%p jsRuntime=%p currentRuntime=%p scheduler=%p", - (unsigned long long)readLoopGen, self, self.bridge, self.readQueue, [self javaScriptRuntimePointer], - currentRuntime, jsScheduler.get()); while (true) { { __typeof__(self) strongSelf = weakSelf; if (!strongSelf || !strongSelf.bridge) { - NSLog(@"ReadArr loop[%llu] exit: bridge dead self=%p bridge=%p totalErrors=%llu consecutiveErrors=%llu jsRuntime=%p currentRuntime=%p", - (unsigned long long)readLoopGen, strongSelf, strongSelf.bridge, (unsigned long long)totalErrors, - (unsigned long long)consecutiveErrors, - strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); + NSLog(@"ReadArr loop exit: bridge dead"); return; } } @@ -390,34 +348,11 @@ - (NSDictionary *)getConstants { NSError *error = nil; NSData *data = KeybaseReadArr(&error); if (error) { - totalErrors++; - consecutiveErrors++; - if (errorStreakStart == 0) { - errorStreakStart = CFAbsoluteTimeGetCurrent(); - } - if (consecutiveErrors <= 5 || consecutiveErrors % 50 == 0) { - __typeof__(self) strongSelf = weakSelf; - os_log(OS_LOG_DEFAULT, "ReadArr loop[%llu] error streak=%llu total=%llu domain=%{public}s code=%ld desc=%{public}s initResult=%{public}s self=%p bridge=%p jsRuntime=%p currentRuntime=%p", - (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, - (unsigned long long)totalErrors, KBStringOrNil(error.domain), (long)error.code, - KBStringOrNil(error.localizedDescription), KBStringOrNil(kbInitResult), - strongSelf, strongSelf.bridge, - strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); - } + NSLog(@"Error reading data: %@", error); // Back off on error to avoid spinning at ~35K/sec and starving the main thread CPU // during foreground re-entry (seen during hang investigation: 419K errors in 12s). [NSThread sleepForTimeInterval:0.1]; } else if (data) { - if (consecutiveErrors > 0) { - CFAbsoluteTime streakDuration = errorStreakStart > 0 ? CFAbsoluteTimeGetCurrent() - errorStreakStart : 0; - __typeof__(self) strongSelf = weakSelf; - NSLog(@"ReadArr loop[%llu] recovered after streak=%llu total=%llu duration=%.3fs nextBytes=%lu self=%p bridge=%p jsRuntime=%p currentRuntime=%p", - (unsigned long long)readLoopGen, (unsigned long long)consecutiveErrors, - (unsigned long long)totalErrors, streakDuration, (unsigned long)data.length, - strongSelf, strongSelf.bridge, strongSelf ? [strongSelf javaScriptRuntimePointer] : nil, currentRuntime); - consecutiveErrors = 0; - errorStreakStart = 0; - } __typeof__(self) strongSelf = weakSelf; if (strongSelf) { [strongSelf sendToJS:data]; @@ -432,19 +367,9 @@ - (NSDictionary *)getConstants { RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; - void *previousRuntime = currentRuntime; _jsRuntime = (jsi::Runtime *)cxxBridge.runtime; - currentRuntime = cxxBridge.runtime; auto &rnRuntime = *(jsi::Runtime *)cxxBridge.runtime; jsScheduler = std::make_shared(rnRuntime, _callInvoker.callInvoker); - uint64_t installCount = gKbInstallCount.fetch_add(1) + 1; - NSLog(@"install[%llu]: self=%p bridge=%p runtime=%p previousRuntime=%p scheduler=%p callInvoker=%p", - (unsigned long long)installCount, self, self.bridge, cxxBridge.runtime, previousRuntime, jsScheduler.get(), - _callInvoker.callInvoker.get()); - if (previousRuntime && previousRuntime != cxxBridge.runtime) { - NSLog(@"install[%llu]: runtime changed without a matching invalidate? previousRuntime=%p newRuntime=%p bridge=%p", - (unsigned long long)installCount, previousRuntime, cxxBridge.runtime, self.bridge); - } // stash the current runtime to keep in sync auto rpcOnGoWrap = [](Runtime &runtime, const Value &thisValue, const Value *arguments, size_t count) -> Value { diff --git a/shared/ios/Keybase.xcodeproj/project.pbxproj b/shared/ios/Keybase.xcodeproj/project.pbxproj index ee9d8d2255fb..13846bfe64fa 100644 --- a/shared/ios/Keybase.xcodeproj/project.pbxproj +++ b/shared/ios/Keybase.xcodeproj/project.pbxproj @@ -41,7 +41,6 @@ DBDCF30E1B8D03DD00BA95D8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */; }; DBDF89F62DF7779900EA18C2 /* Pusher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDF89F52DF7779900EA18C2 /* Pusher.swift */; }; DBF123462DF1234500A12345 /* ShareIntentDonatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF123452DF1234500A12345 /* ShareIntentDonatorImpl.swift */; }; - DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -119,7 +118,6 @@ DBDCF3441B8D04FC00BA95D8 /* Keybase-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Keybase-Bridging-Header.h"; sourceTree = ""; }; DBDF89F52DF7779900EA18C2 /* Pusher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pusher.swift; sourceTree = ""; }; DBF123452DF1234500A12345 /* ShareIntentDonatorImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareIntentDonatorImpl.swift; sourceTree = ""; }; - DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadWatchdog.swift; sourceTree = ""; }; F68DC40B579A1F9AC0F34950 /* Pods_KeybaseShare.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KeybaseShare.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -270,7 +268,6 @@ children = ( 005BF9961BB9C6B000BD8953 /* Keybase.entitlements */, DBB8CC202DF336C200D43215 /* AppDelegate.swift */, - DBMT00002DF336C200D43215 /* MainThreadWatchdog.swift */, DBDCF3081B8D03DD00BA95D8 /* Images.xcassets */, DBDCF3091B8D03DD00BA95D8 /* Info.plist */, DB07050422E21B8B002F273D /* KeepThisFile.swift */, @@ -587,7 +584,6 @@ buildActionMask = 2147483647; files = ( DBB8CC212DF336C200D43215 /* AppDelegate.swift in Sources */, - DBMT00012DF336C200D43215 /* MainThreadWatchdog.swift in Sources */, DB07050522E21B8B002F273D /* KeepThisFile.swift in Sources */, DBDF89F62DF7779900EA18C2 /* Pusher.swift in Sources */, DBF123462DF1234500A12345 /* ShareIntentDonatorImpl.swift in Sources */, diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index 7642a613be33..552e476c294e 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -3,7 +3,6 @@ import Expo import ExpoModulesCore import KBCommon import Keybasego -import OSLog import React import ReactAppDependencyProvider import UIKit @@ -51,36 +50,17 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, private var startupLogFileHandle: FileHandle? private let logQueue = DispatchQueue(label: "kb.startup.log", qos: .utility) - private var watchdog: MainThreadWatchdog? - private var bgEnterWallTime: CFAbsoluteTime = 0 - private var fgEnterTime: CFAbsoluteTime = 0 - public override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - if AppDelegate.appStartTime == 0 { - AppDelegate.appStartTime = CFAbsoluteTimeGetCurrent() - } - let skipLogFile = false self.fsPaths = FsHelper().setupFs(skipLogFile, setupSharedHome: true) FsPathsHolder.shared().fsPaths = self.fsPaths self.writeStartupTimingLog("didFinishLaunchingWithOptions start") - let wd = MainThreadWatchdog( - appStartTime: AppDelegate.appStartTime, - writeLog: { [weak self] msg in - self?.writeStartupTimingLog(msg) - }) - wd.start(context: "cold start") - self.watchdog = wd self.didLaunchSetupBefore() - // Install the SIGUSR1 stack-capture handler after KeybaseInit — Go's runtime - // initializes its own signal handlers during KeybaseInit and would overwrite - // anything registered before it. - wd.install() if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { let notificationDict = Dictionary( @@ -154,9 +134,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, /////// KB specific - private static var appStartTime: CFAbsoluteTime = 0 - private static let initLog = OSLog(subsystem: "keybase", category: "init") - private static let logDateFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" @@ -256,12 +233,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, if let err { let initResult = "FAILED: \(err.localizedDescription) (code=\(err.code) domain=\(err.domain))" KbSetInitResult(initResult) - // Log to system log with public annotation so it's not redacted in logarchive. - os_log( - .error, log: AppDelegate.initLog, - "KeybaseInit FAILED: %{public}@ (code=%ld domain=%{public}@)", - err.localizedDescription, err.code, err.domain) - // Also write to ios.log so it's captured in xcappdata even without a device attached. + NSLog( + "KeybaseInit FAILED: %@ (code=%ld domain=%@)", err.localizedDescription, err.code, + err.domain) self.writeStartupTimingLog("KeybaseInit \(initResult)") } else { KbSetInitResult("succeeded") @@ -280,8 +254,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, case .inactive: Keybasego.KeybaseSetAppStateInactive() default: Keybasego.KeybaseSetAppStateForeground() } - NSLog("notifyAppState: done") - Keybasego.KeybaseFlushLogs() } func didLaunchSetupBefore() { @@ -466,8 +438,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, } public override func applicationDidEnterBackground(_ application: UIApplication) { - bgEnterWallTime = CFAbsoluteTimeGetCurrent() - watchdog?.start(context: "background entered") application.ignoreSnapshotOnNextApplicationLaunch() NSLog("applicationDidEnterBackground: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() @@ -477,7 +447,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, NSLog("applicationDidEnterBackground: notifying go.") let requestTime = Keybasego.KeybaseAppDidEnterBackground() NSLog("applicationDidEnterBackground: after notifying go.") - Keybasego.KeybaseFlushLogs() if requestTime && (self.shutdownTask == UIBackgroundTaskIdentifier.invalid) { let app = UIApplication.shared @@ -503,15 +472,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, } public override func applicationDidBecomeActive(_ application: UIApplication) { - watchdog?.stop() - Keybasego.KeybaseFlushLogs() - let elapsed = CFAbsoluteTimeGetCurrent() - AppDelegate.appStartTime - let fgGap = fgEnterTime > 0 ? CFAbsoluteTimeGetCurrent() - fgEnterTime : 0 - writeStartupTimingLog( - String( - format: "applicationDidBecomeActive: %.1fms after launch, %.0fms after willEnterForeground", - elapsed * 1000, fgGap * 1000)) - NSLog("[Startup] applicationDidBecomeActive: %.0fms after willEnterForeground", fgGap * 1000) NSLog("applicationDidBecomeActive: hiding keyz screen.") hideCover() NSLog("applicationDidBecomeActive: notifying service.") @@ -545,11 +505,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, } public override func applicationWillEnterForeground(_ application: UIApplication) { - fgEnterTime = CFAbsoluteTimeGetCurrent() - let bgDuration = fgEnterTime - bgEnterWallTime - NSLog( - "applicationWillEnterForeground: start, hiding keyz screen (%.1fs since background)", - bgDuration) + NSLog("applicationWillEnterForeground: hiding keyz screen") hideCover() NSLog("applicationWillEnterForeground: done") } diff --git a/shared/ios/Keybase/Fs.swift b/shared/ios/Keybase/Fs.swift index 39700b52324f..690768cf6f88 100644 --- a/shared/ios/Keybase/Fs.swift +++ b/shared/ios/Keybase/Fs.swift @@ -1,12 +1,15 @@ import Foundation @objc class FsHelper: NSObject { - @objc func setupFs(_ skipLogFile: Bool, setupSharedHome shouldSetupSharedHome: Bool) -> [String: String] { + @objc func setupFs(_ skipLogFile: Bool, setupSharedHome shouldSetupSharedHome: Bool) -> [String: + String] + { let setupFsStartTime = CFAbsoluteTimeGetCurrent() NSLog("setupFs: starting") var home = NSHomeDirectory() - let sharedURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.keybase") + let sharedURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.keybase") var sharedHome = sharedURL?.relativePath ?? "" home = setupAppHome(home: home, sharedHome: sharedHome) @@ -18,9 +21,12 @@ import Foundation // Put logs in a subdir that is entirely background readable let oldLogURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] .appendingPathComponent("Keybase") - let serviceLogFile = skipLogFile ? "" : oldLogURL - .appendingPathComponent("logs") - .appendingPathComponent("ios.log").path + let serviceLogFile = + skipLogFile + ? "" + : oldLogURL + .appendingPathComponent("logs") + .appendingPathComponent("ios.log").path let logDirURL = oldLogURL.appendingPathComponent("logs") if !skipLogFile { @@ -47,7 +53,8 @@ import Foundation "kbfs_settings", "synced_tlf_config", ].forEach { - createBackgroundReadableDirectory(path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) + createBackgroundReadableDirectory( + path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) } // Log and avatar dirs live under the caches dir, not Application Support. // This must run after the cleanup above so that any surviving ios.log from a @@ -55,7 +62,8 @@ import Foundation // protection downgraded to completeUntilFirstUserAuthentication before // KeybaseInit tries to open it on a locked device. createBackgroundReadableDirectory(path: logDirURL.path, setAllFiles: true) - createBackgroundReadableDirectory(path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) + createBackgroundReadableDirectory( + path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) let setupFsElapsed = CFAbsoluteTimeGetCurrent() - setupFsStartTime NSLog("setupFs: completed in %.3f seconds", setupFsElapsed) @@ -63,7 +71,7 @@ import Foundation return [ "home": home, "sharedHome": sharedHome, - "logFile": serviceLogFile + "logFile": serviceLogFile, ] } @@ -87,9 +95,12 @@ import Foundation // directory accessible as long as the user has unlocked the phone once. The // files are still stored on the disk encrypted (note for the chat database, // it means we are encrypting it twice), and are inaccessible otherwise. - let noProt = [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication] + let noProt = [ + FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication + ] NSLog("creating background readable directory: path: \(path) setAllFiles: \(setAllFiles)") - _ = try? fm.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: noProt) + _ = try? fm.createDirectory( + atPath: path, withIntermediateDirectories: true, attributes: noProt) do { try fm.setAttributes(noProt, ofItemAtPath: path) } catch { @@ -116,7 +127,9 @@ import Foundation } } let dirElapsed = CFAbsoluteTimeGetCurrent() - dirStartTime - NSLog("createBackgroundReadableDirectory completed for: \(path), processed \(fileCount) files, total: %.3f seconds", dirElapsed) + NSLog( + "createBackgroundReadableDirectory completed for: \(path), processed \(fileCount) files, total: %.3f seconds", + dirElapsed) } else { NSLog("Error creating enumerator for path: \(path)") } @@ -193,7 +206,8 @@ import Foundation _ = addSkipBackupAttribute(to: sharedKeybasePath) guard maybeMigrateDirectory(source: appKeybasePath, dest: sharedKeybasePath), - maybeMigrateDirectory(source: appEraseableKVPath, dest: sharedEraseableKVPath) else { + maybeMigrateDirectory(source: appEraseableKVPath, dest: sharedEraseableKVPath) + else { return home } diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift deleted file mode 100644 index 7fe68237d68c..000000000000 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ /dev/null @@ -1,221 +0,0 @@ -import Darwin -import UIKit - -// File-scope globals required by the SIGUSR1 signal handler. -// Signal handlers cannot safely reference Swift objects, so these must be C-compatible globals. -private let kMaxStackFrames: Int32 = 128 -private var gMainStackFrames = [UnsafeMutableRawPointer?]( - repeating: nil, count: Int(kMaxStackFrames)) -private var gMainStackFrameCount: Int32 = 0 -private var gMainStackReady: Bool = false - -// Monitors the main thread for hangs by pinging it every second from a background thread. -// Captures a stack trace via SIGUSR1 on first hang detection. -// -// Lifecycle: -// 1. Call install() once on the main thread at app startup. -// 2. Call start(context:) when entering background (or at cold start). -// 3. Call stop() at the top of applicationDidBecomeActive. -class MainThreadWatchdog { - private var active = false - private let lock = NSLock() - private var lastPong: CFAbsoluteTime = 0 - private var bgEnterTime: CFAbsoluteTime = 0 - private var isBackgroundContext = false - - private var mainThreadPthread: pthread_t? - private let appStartTime: CFAbsoluteTime - private let writeLog: (String) -> Void - - init(appStartTime: CFAbsoluteTime, writeLog: @escaping (String) -> Void) { - self.appStartTime = appStartTime - self.writeLog = writeLog - } - - // Must be called from the main thread. Captures pthread_self() and installs - // the SIGUSR1 handler that records the main thread stack on demand. - func install() { - mainThreadPthread = pthread_self() - // Force lazy initialization of gMainStackFrames before registering the signal handler. - // Swift file-scope globals initialize via dispatch_once on first access; triggering that - // inside a signal handler while the ObjC runtime lock is held causes - // _os_unfair_lock_recursive_abort (crash seen in PID 31439, 2026-03-28). - _ = gMainStackFrames.count - signal(SIGUSR1) { _ in - gMainStackFrameCount = backtrace(&gMainStackFrames, kMaxStackFrames) - gMainStackReady = true - } - } - - func start(context: String) { - lock.lock() - if active { - lock.unlock() - return - } - active = true - let now = CFAbsoluteTimeGetCurrent() - lastPong = now - bgEnterTime = now - isBackgroundContext = (context == "background entered") - lock.unlock() - - writeLog("Watchdog: started (\(context))") - - DispatchQueue(label: "kb.startup.watchdog", qos: .utility).async { [weak self] in - self?.run() - } - } - - func stop() { - lock.lock() - let wasActive = active - active = false - lock.unlock() - if wasActive { - writeLog("Watchdog: stopped") - } - } - - // MARK: - Private - - private func run() { - var lastLogTime: CFAbsoluteTime = 0 - - while true { - Thread.sleep(forTimeInterval: 0.5) - - lock.lock() - let isActive = active - let lastPong = self.lastPong - let bgEnterTime = self.bgEnterTime - let isBackgroundContext = self.isBackgroundContext - lock.unlock() - guard isActive else { break } - - let now = CFAbsoluteTimeGetCurrent() - let blockDuration = now - lastPong - - // If the process was suspended by iOS, blockDuration reflects the suspension gap rather - // than a real main-thread hang. Two cases: - // 1. Background-entered watchdog: ANY gap >= 3s is a suspension — iOS suspends apps - // aggressively even after a few seconds in the background, so the 30s threshold - // below is too coarse and produces false positives for 5–29s suspensions. - // 2. Cold-start or foreground watchdog: use the 30s threshold as before. - if blockDuration > 30.0 || (isBackgroundContext && blockDuration >= 3.0) { - let bgElapsedSec = now - bgEnterTime - let msg = String( - format: "Watchdog: process resumed after %.0fs suspension (%.0fs since background)", - blockDuration, bgElapsedSec) - NSLog("[Startup] %@", msg) - // Capture a stack trace even on suspected suspension: if the main thread is actually - // hung during foreground re-entry (not truly suspended by iOS), the trace will show it. - // If truly suspended the signal will time out and log "stack capture timed out". - captureAndLogStackTrace() - lock.lock() - self.lastPong = now - lock.unlock() - lastLogTime = 0 - DispatchQueue.main.async { [weak self] in - guard let self else { return } - self.lock.lock() - self.lastPong = CFAbsoluteTimeGetCurrent() - self.lock.unlock() - } - continue - } - - let totalElapsedMs = (now - appStartTime) * 1000 - - if blockDuration >= 1.0 { - // Sample every 1s for the duration of the hang so we capture how the main thread - // evolves (e.g. keychain IPC → rendering → idle) rather than a single snapshot. - if lastLogTime == 0 || (now - lastLogTime) >= 1.0 { - let bgElapsedSec = now - bgEnterTime - let msg = String( - format: - "Watchdog: main thread blocked %.1fs after foreground resume (%.0fs since background, %.0fms since launch)", - blockDuration, bgElapsedSec, totalElapsedMs) - NSLog("[Startup] %@", msg) - // Enqueue a write for when the main thread recovers - DispatchQueue.main.async { [weak self] in - self?.writeLog(msg) - } - // Capture a stack trace on every sample interval, not just the first. - captureAndLogStackTrace() - lastLogTime = now - } - } else { - if lastLogTime != 0 { - let bgElapsedSec = now - bgEnterTime - let msg = String( - format: "Watchdog: main thread unblocked (%.0fs since background, %.0fms since launch)", - bgElapsedSec, totalElapsedMs) - NSLog("[Startup] %@", msg) - DispatchQueue.main.async { [weak self] in - self?.writeLog(msg) - } - lastLogTime = 0 - } - } - - // Ping: ask main thread to update the pong time - DispatchQueue.main.async { [weak self] in - guard let self else { return } - self.lock.lock() - self.lastPong = CFAbsoluteTimeGetCurrent() - self.lock.unlock() - } - } - } - - // Send SIGUSR1 to the main thread, wait briefly for the handler to run, then log the stack. - // Frames are written via NSLog immediately (so they survive if the app is killed before the - // main thread recovers) and also dispatched to writeLog for ios.log / logsend. - private func captureAndLogStackTrace() { - gMainStackReady = false - guard let tid = mainThreadPthread else { - NSLog("[Startup] Watchdog: main thread pthread not captured") - DispatchQueue.main.async { [weak self] in - self?.writeLog("Watchdog: main thread pthread not captured") - } - return - } - pthread_kill(tid, SIGUSR1) - // Spin up to 200ms for the signal handler to complete - for _ in 0..<20 { - if gMainStackReady { break } - Thread.sleep(forTimeInterval: 0.01) - } - guard gMainStackReady else { - NSLog("[Startup] Watchdog: stack capture timed out") - DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: stack capture timed out") - } - return - } - let count = Int(gMainStackFrameCount) - // Collect the binary load slide so addresses can be symbolicated offline: - // atos -o Keybase.app.dSYM/Contents/Resources/DWARF/Keybase -l
- let slide = _dyld_get_image_vmaddr_slide(0) - // Build the frame strings synchronously while the globals are still valid. - var lines = [String]() - lines.append( - String(format: "Watchdog: main thread stack trace (%d frames, slide=0x%lx):", count, slide)) - gMainStackFrames.withUnsafeMutableBufferPointer { buf in - if let syms = backtrace_symbols(buf.baseAddress, Int32(count)) { - for i in 0..= 0) ? NSNumber(value: badgeCount) : nil content.body = msg ?? "" content.userInfo = ["convID": convID ?? "", "type": typ ?? ""] - let request = UNNotificationRequest(identifier: ident ?? UUID().uuidString, content: content, trigger: nil) + let request = UNNotificationRequest( + identifier: ident ?? UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { error in if let error = error { NSLog("local notification failed: %@", error.localizedDescription) @@ -27,12 +31,15 @@ class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { if notification.isPlaintext && !message.plaintext.isEmpty { let username = message.from?.keybaseUsername ?? "" let convName = notification.conversationName - msg = (username == convName || convName.isEmpty) + msg = + (username == convName || convName.isEmpty) ? "\(username): \(message.plaintext)" : "\(username) (\(convName)): \(message.plaintext)" } else { msg = message.serverMessage } - localNotification(ident, msg: msg, badgeCount: notification.badgeCount, soundName: notification.soundName, convID: notification.convID, typ: "chat.newmessage") + localNotification( + ident, msg: msg, badgeCount: notification.badgeCount, soundName: notification.soundName, + convID: notification.convID, typ: "chat.newmessage") } } diff --git a/shared/ios/Keybase/ShareIntentDonatorImpl.swift b/shared/ios/Keybase/ShareIntentDonatorImpl.swift index c5bde51cd30a..dd9f1aaa1261 100644 --- a/shared/ios/Keybase/ShareIntentDonatorImpl.swift +++ b/shared/ios/Keybase/ShareIntentDonatorImpl.swift @@ -51,7 +51,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto NSLog("ShareIntentDonator: donateShareConversations: empty conversations array") return } - NSLog("ShareIntentDonator: donateShareConversations: donating %d conversations", conversations.count) + NSLog( + "ShareIntentDonator: donateShareConversations: donating %d conversations", conversations.count + ) donateConversations(conversations) } @@ -97,7 +99,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto } /// Loads avatar URL(s) and composites them. Apple requires a non-nil image for share sheet suggestions. - private func loadAvatars(urls: [URL], intent: INSendMessageIntent, completion: @escaping () -> Void) { + private func loadAvatars( + urls: [URL], intent: INSendMessageIntent, completion: @escaping () -> Void + ) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in let images = urls.compactMap { (try? Data(contentsOf: $0)).flatMap { UIImage(data: $0) } } if let combined = self?.compositeAvatarImages(images), let data = combined.pngData() { @@ -129,7 +133,8 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let size: CGFloat = 192 let circleSize = size * 0.65 let leftRect = CGRect(origin: .zero, size: CGSize(width: circleSize, height: circleSize)) - let rightRect = CGRect(x: size - circleSize, y: size - circleSize, width: circleSize, height: circleSize) + let rightRect = CGRect( + x: size - circleSize, y: size - circleSize, width: circleSize, height: circleSize) let fullRect = CGRect(origin: .zero, size: CGSize(width: size, height: size)) let format = UIGraphicsImageRendererFormat() format.opaque = false @@ -142,7 +147,8 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let scale = max(size / imgSize.width, size / imgSize.height) let scaledW = imgSize.width * scale let scaledH = imgSize.height * scale - let drawRect = CGRect(x: (size - scaledW) / 2, y: (size - scaledH) / 2, width: scaledW, height: scaledH) + let drawRect = CGRect( + x: (size - scaledW) / 2, y: (size - scaledH) / 2, width: scaledW, height: scaledH) self.drawImageInCircle(img, in: fullRect, drawRect: drawRect, context: cgContext) } else { self.drawImageInCircle(images[0], in: leftRect, drawRect: leftRect, context: cgContext) @@ -152,7 +158,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto } /// Draws an image clipped to an oval. Uses aspect fill when drawRect differs from clip rect. - private func drawImageInCircle(_ image: UIImage, in clipRect: CGRect, drawRect: CGRect, context: CGContext) { + private func drawImageInCircle( + _ image: UIImage, in clipRect: CGRect, drawRect: CGRect, context: CGContext + ) { context.saveGState() UIBezierPath(ovalIn: clipRect).addClip() image.draw(in: drawRect) @@ -163,7 +171,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let interaction = INInteraction(intent: intent, response: nil) interaction.donate { error in if let error = error { - NSLog("ShareIntentDonator: donateIntent failed for %@: %@", intent.conversationIdentifier ?? "?", error.localizedDescription) + NSLog( + "ShareIntentDonator: donateIntent failed for %@: %@", + intent.conversationIdentifier ?? "?", error.localizedDescription) } } } diff --git a/shared/ios/KeybaseShare/ShareViewController.swift b/shared/ios/KeybaseShare/ShareViewController.swift index 163e8a5e7668..041e53163cec 100644 --- a/shared/ios/KeybaseShare/ShareViewController.swift +++ b/shared/ios/KeybaseShare/ShareViewController.swift @@ -8,10 +8,10 @@ import Foundation import Intents -import UIKit -import MobileCoreServices -import Keybasego import KBCommon +import Keybasego +import MobileCoreServices +import UIKit @objc(ShareViewController) public class ShareViewController: UIViewController { @@ -33,7 +33,11 @@ public class ShareViewController: UIViewController { let sel = #selector(UIApplication.open(_:options:completionHandler:)) if r.responds(to: sel) { let imp = r.method(for: sel) - typealias Func = @convention(c) (AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], ((Bool) -> Void)?) -> Void + typealias Func = + @convention(c) ( + AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], + ((Bool) -> Void)? + ) -> Void let f = unsafeBitCast(imp, to: Func.self) f(r, sel, url, [:], nil) return @@ -54,7 +58,9 @@ public class ShareViewController: UIViewController { } func showProgressView() { - let alertController = UIAlertController(title: "Working on it", message: "\n\nPreparing content for sharing into Keybase.", preferredStyle: .alert) + let alertController = UIAlertController( + title: "Working on it", message: "\n\nPreparing content for sharing into Keybase.", + preferredStyle: .alert) alert = alertController let spinner = UIActivityIndicatorView(style: .medium) spinner.translatesAutoresizingMaskIntoConstraints = false @@ -62,7 +68,7 @@ public class ShareViewController: UIViewController { alertController.view.addSubview(spinner) NSLayoutConstraint.activate([ spinner.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: alertController.view.centerYAnchor, constant: -8) + spinner.centerYAnchor.constraint(equalTo: alertController.view.centerYAnchor, constant: -8), ]) present(alertController, animated: true, completion: nil) } @@ -76,9 +82,10 @@ public class ShareViewController: UIViewController { if let intent = extensionContext?.intent as? INSendMessageIntent { selectedConvID = intent.conversationIdentifier } - let itemArrs = extensionContext?.inputItems.compactMap { - ($0 as? NSExtensionItem)?.attachments - } ?? [] + let itemArrs = + extensionContext?.inputItems.compactMap { + ($0 as? NSExtensionItem)?.attachments + } ?? [] weak var weakSelf = self iph = ItemProviderHelper(forShare: true, withItems: itemArrs) { From 70cbb6ec7c04ed75dc7538ad7f04ac2bc9107b1d Mon Sep 17 00:00:00 2001 From: zoom-ua <65734190+zoom-ua@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:58:39 -0400 Subject: [PATCH 42/42] rm .cursorrules (#29110) --- .cursorrules | 132 --------------------------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 442b4c60c093..000000000000 --- a/.cursorrules +++ /dev/null @@ -1,132 +0,0 @@ -# Cursor Rules for Keybase Client Repository - -## Downloading CI Test Failure Logs (citigo logs) - -When test failures occur, you'll see suggested commands like: - -``` -curl -s -o - https://ci-fail-logs.s3.amazonaws.com/citogo-PR_28719-4-kbfs_libdokan-TestStatRoot-puco3td7mech4ilrcdhabhsklm25vgk2.gz | zcat -d | less -``` - -### ⚠️ COMMON PITFALLS TO AVOID: - -1. **DO NOT run curl commands in the sandbox** - Use `required_permissions: ["all"]` to bypass SSL certificate access restrictions -2. **DO NOT stream logs to stdout/less** - Save them locally first to avoid repeated downloads -3. **DO NOT use `-k` flag** - Use proper permissions instead to maintain SSL security - -### ✅ CORRECT WORKFLOW: - -#### Step 1: Extract the Log Information - -When you see a test failure with a fetch command, identify: - -- The full URL (e.g., `https://ci-fail-logs.s3.amazonaws.com/citogo-PR_28719-4-...`) -- The test name (e.g., `TestStatRoot`) - -#### Step 2: Download and Save Locally - -Use `run_terminal_cmd` with **all permissions** to bypass sandbox SSL certificate restrictions: - -```bash -# Download the .gz file locally first -curl -s -o /path/to/repo/test-failure-TestName.gz https://ci-fail-logs.s3.amazonaws.com/citigo-... -``` - -**Important**: - -- **ALWAYS use `required_permissions: ["all"]`** - The sandbox blocks access to `/etc/ssl/cert.pem`, causing SSL verification to fail -- This allows curl to properly verify SSL certificates without using `-k` (insecure mode) -- Save files to the repository root with descriptive names -- Keep the .gz extension for the downloaded file -- For multiple logs, chain commands with `&&` to download all at once - -#### Step 3: Decompress Locally - -Once downloaded, decompress without network access: - -```bash -# Decompress the file -gunzip -c test-failure-TestName.gz > test-failure-TestName.log -``` - -Or in one step if the file is already downloaded: - -```bash -zcat test-failure-TestName.gz > test-failure-TestName.log -``` - -#### Step 4: Read the Decompressed Log - -Use the `read_file` tool to read the decompressed `.log` file directly. - -### EXAMPLE COMPLETE WORKFLOW: - -``` -User reports: "TestStatRoot failed, logs at: curl -s -o - https://ci-fail-logs.s3.amazonaws.com/citogo-PR_28719-4-kbfs_libdokan-TestStatRoot-puco3td7mech4ilrcdhabhsklm25vgk2.gz | zcat -d | less" - -Step 1: Download bypassing sandbox (required for SSL cert access) -run_terminal_cmd( - command: "curl -s -o TestStatRoot-failure.gz https://ci-fail-logs.s3.amazonaws.com/citogo-PR_28719-4-kbfs_libdokan-TestStatRoot-puco3td7mech4ilrcdhabhsklm25vgk2.gz", - required_permissions: ["all"] -) - -Step 2: Decompress locally (no special permissions needed) -run_terminal_cmd( - command: "gunzip -c TestStatRoot-failure.gz > TestStatRoot-failure.log" -) - -Step 3: Read the log file -read_file(target_file: "TestStatRoot-failure.log") -``` - -### EXAMPLE: Multiple Logs at Once - -``` -For multiple test failures, download all logs in one command: - -run_terminal_cmd( - command: "cd /path/to/repo && curl -s -o Test1.gz URL1 && curl -s -o Test2.gz URL2 && curl -s -o Test3.gz URL3", - required_permissions: ["all"] -) - -Then decompress all at once: -run_terminal_cmd( - command: "cd /path/to/repo && gunzip -c Test1.gz > Test1.log && gunzip -c Test2.gz > Test2.log && gunzip -c Test3.gz > Test3.log" -) -``` - -### TROUBLESHOOTING: - -**If you get certificate errors (exit code 77):** - -- Root cause: Sandbox blocks access to `/etc/ssl/cert.pem` (system CA certificates) -- Solution: Use `required_permissions: ["all"]` to bypass sandbox restrictions -- This allows curl to access system SSL certificates for proper verification -- Alternative (not recommended): Use `-k` flag to skip certificate verification entirely - -**If sandbox errors occur:** - -- Ensure you're using `required_permissions: ["all"]` for curl commands -- The sandbox allows network access with `["network"]` but still blocks system cert access - -**If the file is corrupted:** - -- Re-download with verbose output: `curl -v -o file.gz https://...` -- Check file size: `ls -lh file.gz` - -### FILE NAMING CONVENTION: - -Use descriptive names that include the test name: - -- ✅ `TestStatRoot-failure.log` -- ✅ `test-fail-PR28719-TestStatRoot.gz` -- ❌ `test-fail.gz` (too generic) -- ❌ `temp.log` (not descriptive) - -### CLEANUP: - -After analyzing logs, consider cleaning up large log files: - -```bash -rm *.gz *.log -```