Files
damus/damus/damusApp.swift
Daniel D’Aquino 368f94a209 Background 0xdead10cc crash fix
This commit fixes the background crashes with termination code
0xdead10cc.

Those crashes were caused by the fact that NostrDB was being stored on
the shared app container (Because our app extensions need NostrDB
data), and iOS kills any process that holds a file lock after the
process is backgrounded.

Other developers in the field have run into similar problems in the past
(with shared SQLite databases or shared SwiftData), and they generally
recommend not to place those database in shared containers at all,
mentioning that 0xdead10cc crashes are almost inevitable otherwise:

- https://ryanashcraft.com/sqlite-databases-in-app-group-containers/
- https://inessential.com/2020/02/13/how_we_fixed_the_dreaded_0xdead10cc_cras.html

Since iOS aggressively backgrounds and terminates processes with tight
timing constraints that are mostly outside our control (despite using
Apple's recommended mechanisms, such as requesting more time to perform
closing operations), this fix aims to address the issue by a different
storage architecture.

Instead of keeping NostrDB data on the shared app container and handling
the closure/opening of the database with the app lifecycle signals, keep
the main NostrDB database file in the app's private container, and instead
take periodic read-only snapshots of NostrDB in the shared container, so as
to allow extensions to have recent NostrDB data without all the
complexities of keeping the main file in the shared container.

This does have the tradeoff that more storage will be used by NostrDB
due to file duplication, but that can be mitigated via other techniques
if necessary.

Closes: https://github.com/damus-io/damus/issues/2638
Closes: https://github.com/damus-io/damus/issues/3463
Changelog-Fixed: Fixed background crashes with error code 0xdead10cc
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2026-01-02 20:49:13 -08:00

135 lines
5.1 KiB
Swift

//
// damusApp.swift
// damus
//
// Created by William Casarin on 2022-04-01.
//
import Kingfisher
import SwiftUI
import StoreKit
@main
struct damusApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
MainView(appDelegate: appDelegate)
}
}
}
struct MainView: View {
@State var needs_setup = false;
@State var keypair: Keypair? = nil;
@StateObject private var orientationTracker = OrientationTracker()
var appDelegate: AppDelegate
var body: some View {
Group {
if let kp = keypair, !needs_setup {
ContentView(keypair: kp, appDelegate: appDelegate)
.environmentObject(orientationTracker)
} else {
SetupView()
.onReceive(handle_notify(.login)) { notif in
needs_setup = false
keypair = get_saved_keypair()
if keypair == nil, let tempkeypair = notif.to_full()?.to_keypair() {
keypair = tempkeypair
}
}
}
}
.dynamicTypeSize(.xSmall ... .xxxLarge)
.onReceive(handle_notify(.logout)) { () in
try? clear_keypair()
keypair = nil
SuggestedHashtagsView.lastRefresh_hashtags.removeAll()
// We need to disconnect and reconnect to all relays when the user signs out
// This is to conform to NIP-42 and ensure we aren't persisting old connections
notify(.disconnect_relays)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationTracker.setDeviceMajorAxis()
}
.onAppear {
orientationTracker.setDeviceMajorAxis()
keypair = get_saved_keypair()
}
}
}
func registerNotificationCategories() {
// Define the communication category
let communicationCategory = UNNotificationCategory(
identifier: "COMMUNICATION",
actions: [],
intentIdentifiers: ["INSendMessageIntent"],
options: []
)
// Register the category with the notification center
UNUserNotificationCenter.current().setNotificationCategories([communicationCategory])
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var state: DamusState? = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
SKPaymentQueue.default().add(StoreObserver.standard)
registerNotificationCategories()
ImageCacheMigrations.migrateKingfisherCacheIfNeeded()
configureKingfisherCache()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
guard let state else {
return
}
Task {
try await state.push_notification_client.set_device_token(new_device_token: deviceToken)
}
}
// Handle the notification in the foreground state
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// Display the notification in the foreground
completionHandler([.banner, .list, .sound, .badge])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Log.info("App delegate is handling a push notification", for: .push_notifications)
let userInfo = response.notification.request.content.userInfo
guard let notification = LossyLocalNotification.from_user_info(user_info: userInfo) else {
Log.error("App delegate could not decode notification information", for: .push_notifications)
return
}
Log.info("App delegate notifying the app about the received push notification", for: .push_notifications)
Task { await QueueableNotify<LossyLocalNotification>.shared.add(item: notification) }
completionHandler()
}
private func configureKingfisherCache() {
let cachePath = ImageCacheMigrations.kingfisherCachePath()
if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
KingfisherManager.shared.cache = cache
}
}
}
class OrientationTracker: ObservableObject {
var deviceMajorAxis: CGFloat = 0
func setDeviceMajorAxis() {
let bounds = UIScreen.main.bounds
let height = max(bounds.height, bounds.width) /// device's longest dimension
let width = min(bounds.height, bounds.width) /// device's shortest dimension
let orientation = UIDevice.current.orientation
deviceMajorAxis = (orientation == .portrait || orientation == .unknown) ? height : width
}
}