From fdf28aa5bbdf6842e2a6f64f7343b00c14d5aa0e Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:48:53 +0200 Subject: [PATCH] Fix launch crash: recursive dispatch_once between NostrRelayManager and NetworkActivationService (#1343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix launch crash: recursive dispatch_once between NostrRelayManager and NetworkActivationService NostrRelayManager.init() runs applyDefaultRelayPolicy(force: true), which calls dependencies.activationAllowed() when the user has location permission or a mutual favorite. That closure resolves NetworkActivationService.shared, whose init captured NostrRelayManager.shared — re-entering the still-running dispatch_once on the same thread. libdispatch traps on recursive dispatch_once (EXC_BREAKPOINT in _dispatch_once_wait), killing the app ~50ms after launch, before the first frame. Fresh installs were unaffected (no permission, no favorites, so the policy path never touched NetworkActivationService during init), which is why this passed local testing but crashed established TestFlight users on every launch. Two independent TestFlight crash reports on 1.5.2 (1) show the identical stack. Break the cycle by resolving the relay controller lazily: store a provider closure in init and dereference NostrRelayManager.shared on first use (start()/reevaluate()), after both singletons have finished initializing. The injectable test initializer keeps its signature. Co-Authored-By: Claude Fable 5 * Bump version to 1.5.3 Co-Authored-By: Claude Fable 5 --------- Co-authored-by: jack Co-authored-by: Claude Fable 5 --- Configs/Release.xcconfig | 2 +- bitchat.xcodeproj/project.pbxproj | 8 ++++---- bitchat/Services/NetworkActivationService.swift | 10 +++++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Configs/Release.xcconfig b/Configs/Release.xcconfig index e4b48f6e..b87e9064 100644 --- a/Configs/Release.xcconfig +++ b/Configs/Release.xcconfig @@ -1,4 +1,4 @@ -MARKETING_VERSION = 1.5.2 +MARKETING_VERSION = 1.5.3 CURRENT_PROJECT_VERSION = 1 IPHONEOS_DEPLOYMENT_TARGET = 16.0 diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index bfa922c5..5da57510 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -561,7 +561,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -620,7 +620,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -655,7 +655,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; @@ -749,7 +749,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = 1.5.2; + MARKETING_VERSION = 1.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; diff --git a/bitchat/Services/NetworkActivationService.swift b/bitchat/Services/NetworkActivationService.swift index a9795024..1f68af8d 100644 --- a/bitchat/Services/NetworkActivationService.swift +++ b/bitchat/Services/NetworkActivationService.swift @@ -44,7 +44,11 @@ final class NetworkActivationService: ObservableObject { private let permissionProvider: () -> LocationChannelManager.PermissionState private let mutualFavoritesProvider: () -> Set private let torController: NetworkActivationTorControlling - private let relayController: NetworkActivationRelayControlling + // Resolved lazily: NostrRelayManager.init() reads NetworkActivationService.shared + // (via its live dependencies), so capturing NostrRelayManager.shared here would + // re-enter whichever singleton's dispatch_once started first and trap at launch. + private lazy var relayController: NetworkActivationRelayControlling = relayControllerProvider() + private let relayControllerProvider: () -> NetworkActivationRelayControlling private let proxyController: NetworkActivationProxyControlling private let notificationCenter: NotificationCenter @@ -55,7 +59,7 @@ final class NetworkActivationService: ObservableObject { permissionProvider = { LocationChannelManager.shared.permissionState } mutualFavoritesProvider = { FavoritesPersistenceService.shared.mutualFavorites } torController = TorManager.shared - relayController = NostrRelayManager.shared + relayControllerProvider = { NostrRelayManager.shared } proxyController = TorURLSession.shared notificationCenter = .default } @@ -77,7 +81,7 @@ final class NetworkActivationService: ObservableObject { self.permissionProvider = permissionProvider self.mutualFavoritesProvider = mutualFavoritesProvider self.torController = torController - self.relayController = relayController + self.relayControllerProvider = { relayController } self.proxyController = proxyController self.notificationCenter = notificationCenter }