diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index ab55aaf3..49be4e2a 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -41,6 +41,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Register default settings before doing anything else. + UserDefaults.registerDefaults() + DatabaseManager.shared.start { (error) in if let error = error { @@ -58,9 +61,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ServerManager.shared.startDiscovering() - SecureValueTransformer.register() - - UserDefaults.standard.registerDefaults() + SecureValueTransformer.register() if UserDefaults.standard.firstLaunch == nil { diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index 041c62a0..4c0dfe3d 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -12,7 +12,15 @@ import Roxas public extension UserDefaults { + static let shared: UserDefaults = { + guard let appGroup = Bundle.main.appGroups.first else { return .standard } + + let sharedUserDefaults = UserDefaults(suiteName: appGroup)! + return sharedUserDefaults + }() + @NSManaged var firstLaunch: Date? + @NSManaged var requiresAppGroupMigration: Bool @NSManaged var preferredServerID: String? @@ -42,16 +50,20 @@ public extension UserDefaults } @NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber? - func registerDefaults() + class func registerDefaults() { let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0) let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5) let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5) - self.register(defaults: [ + let defaults = [ #keyPath(UserDefaults.isBackgroundRefreshEnabled): true, #keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported, - #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions - ]) + #keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions, + #keyPath(UserDefaults.requiresAppGroupMigration): true + ] + + UserDefaults.standard.register(defaults: defaults) + UserDefaults.shared.register(defaults: defaults) } } diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift index 26c44276..4a99ca0b 100644 --- a/AltStoreCore/Model/DatabaseManager.swift +++ b/AltStoreCore/Model/DatabaseManager.swift @@ -11,7 +11,7 @@ import CoreData import AltSign import Roxas -private class PersistentContainer: RSTPersistentContainer +fileprivate class PersistentContainer: RSTPersistentContainer { override class func defaultDirectoryURL() -> URL { @@ -22,6 +22,11 @@ private class PersistentContainer: RSTPersistentContainer return databaseDirectoryURL } + + class func legacyDirectoryURL() -> URL + { + return super.defaultDirectoryURL() + } } public class DatabaseManager @@ -35,6 +40,9 @@ public class DatabaseManager private var startCompletionHandlers = [(Error?) -> Void]() private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager") + private let coordinator = NSFileCoordinator() + private let coordinatorQueue = OperationQueue() + private init() { self.persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self)) @@ -65,14 +73,21 @@ public extension DatabaseManager guard !self.isStarted else { return finish(nil) } - self.persistentContainer.loadPersistentStores { (description, error) in - guard error == nil else { return finish(error!) } - - self.prepareDatabase() { (result) in - switch result - { - case .failure(let error): finish(error) - case .success: finish(nil) + self.migrateDatabaseToAppGroupIfNeeded { (result) in + switch result + { + case .failure(let error): finish(error) + case .success: + self.persistentContainer.loadPersistentStores { (description, error) in + guard error == nil else { return finish(error!) } + + self.prepareDatabase() { (result) in + switch result + { + case .failure(let error): finish(error) + case .success: finish(nil) + } + } } } } @@ -290,4 +305,75 @@ private extension DatabaseManager } } } + + func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result) -> Void) + { + guard UserDefaults.shared.requiresAppGroupMigration else { return completion(.success(())) } + + func finish(_ result: Result) + { + switch result + { + case .failure(let error): completion(.failure(error)) + case .success: + UserDefaults.shared.requiresAppGroupMigration = false + completion(.success(())) + } + } + + let previousDatabaseURL = PersistentContainer.legacyDirectoryURL().appendingPathComponent("AltStore.sqlite") + let databaseURL = PersistentContainer.defaultDirectoryURL().appendingPathComponent("AltStore.sqlite") + + let previousAppsDirectoryURL = InstalledApp.legacyAppsDirectoryURL + let appsDirectoryURL = InstalledApp.appsDirectoryURL + + let databaseIntent = NSFileAccessIntent.writingIntent(with: databaseURL, options: [.forReplacing]) + let appsIntent = NSFileAccessIntent.writingIntent(with: appsDirectoryURL, options: [.forReplacing]) + + self.coordinator.coordinate(with: [databaseIntent, appsIntent], queue: self.coordinatorQueue) { (error) in + do + { + if let error = error + { + throw error + } + + let description = NSPersistentStoreDescription(url: previousDatabaseURL) + + // Disable WAL to remove extra files automatically during migration. + description.setOption(["journal_mode": "DELETE"] as NSDictionary, forKey: NSSQLitePragmasOption) + + let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.persistentContainer.managedObjectModel) + + // Migrate database + if FileManager.default.fileExists(atPath: previousDatabaseURL.path) + { + if FileManager.default.fileExists(atPath: databaseURL.path, isDirectory: nil) + { + try FileManager.default.removeItem(at: databaseURL) + } + + let previousDatabase = try persistentStoreCoordinator.addPersistentStore(ofType: description.type, configurationName: description.configuration, at: description.url, options: description.options) + + // Pass nil options to prevent later error due to self.persistentContainer using WAL. + try persistentStoreCoordinator.migratePersistentStore(previousDatabase, to: databaseURL, options: nil, withType: NSSQLiteStoreType) + + try FileManager.default.removeItem(at: previousDatabaseURL) + } + + // Migrate apps + if FileManager.default.fileExists(atPath: previousAppsDirectoryURL.path, isDirectory: nil) + { + _ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL) + } + + finish(.success(())) + } + catch + { + print("Failed to migrate database to app group:", error) + finish(.failure(error)) + } + } + } } diff --git a/AltStoreCore/Model/InstalledApp.swift b/AltStoreCore/Model/InstalledApp.swift index ca542ae9..2b521748 100644 --- a/AltStoreCore/Model/InstalledApp.swift +++ b/AltStoreCore/Model/InstalledApp.swift @@ -228,6 +228,12 @@ public extension InstalledApp return appsDirectoryURL } + class var legacyAppsDirectoryURL: URL { + let baseDirectory = FileManager.default.applicationSupportDirectory + let appsDirectoryURL = baseDirectory.appendingPathComponent("Apps") + return appsDirectoryURL + } + class func fileURL(for app: AppProtocol) -> URL { let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")