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>
135 lines
5.1 KiB
Swift
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
|
|
}
|
|
}
|