Files
SideStore/Sources/SideStoreCore/Model/DatabaseManager.swift

367 lines
16 KiB
Swift
Raw Normal View History

2019-05-20 21:24:53 +02:00
//
// DatabaseManager.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import AltSign
2019-05-20 21:24:53 +02:00
import Roxas
2023-03-01 00:48:36 -05:00
private extension CFNotificationName {
static let willAccessDatabase = CFNotificationName("com.rileytestut.AltStore.WillAccessDatabase" as CFString)
}
2023-03-01 00:48:36 -05:00
private let ReceivedWillAccessDatabaseNotification: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { _, _, _, _, _ in
DatabaseManager.shared.receivedWillAccessDatabaseNotification()
}
2023-03-01 00:48:36 -05:00
private class PersistentContainer: RSTPersistentContainer {
override class func defaultDirectoryURL() -> URL {
guard let sharedDirectoryURL = FileManager.default.altstoreSharedDirectory else { return super.defaultDirectoryURL() }
2023-03-01 00:48:36 -05:00
let databaseDirectoryURL = sharedDirectoryURL.appendingPathComponent("Database")
try? FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil)
return databaseDirectoryURL
}
2023-03-01 00:48:36 -05:00
class func legacyDirectoryURL() -> URL {
super.defaultDirectoryURL()
}
}
2023-03-01 00:48:36 -05:00
public class DatabaseManager {
2019-05-20 21:24:53 +02:00
public static let shared = DatabaseManager()
2023-03-01 00:48:36 -05:00
2019-05-20 21:24:53 +02:00
public let persistentContainer: RSTPersistentContainer
2023-03-01 00:48:36 -05:00
2019-05-20 21:24:53 +02:00
public private(set) var isStarted = false
2023-03-01 00:48:36 -05:00
private var startCompletionHandlers = [(Error?) -> Void]()
2020-07-13 17:59:52 -07:00
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
2023-03-01 00:48:36 -05:00
private let coordinator = NSFileCoordinator()
private let coordinatorQueue = OperationQueue()
2023-03-01 00:48:36 -05:00
private var ignoreWillAccessDatabaseNotification = false
2023-03-01 00:48:36 -05:00
private init() {
persistentContainer = PersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
persistentContainer.preferredMergePolicy = MergePolicy()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, ReceivedWillAccessDatabaseNotification, CFNotificationName.willAccessDatabase.rawValue, nil, .deliverImmediately)
2019-05-20 21:24:53 +02:00
}
}
2023-03-01 00:48:36 -05:00
public extension DatabaseManager {
func start(completionHandler: @escaping (Error?) -> Void) {
func finish(_ error: Error?) {
dispatchQueue.async {
if error == nil {
2020-07-13 17:59:52 -07:00
self.isStarted = true
}
2023-03-01 00:48:36 -05:00
2020-07-13 17:59:52 -07:00
self.startCompletionHandlers.forEach { $0(error) }
self.startCompletionHandlers.removeAll()
}
}
2023-03-01 00:48:36 -05:00
dispatchQueue.async {
2020-07-13 17:59:52 -07:00
self.startCompletionHandlers.append(completionHandler)
guard self.startCompletionHandlers.count == 1 else { return }
2023-03-01 00:48:36 -05:00
2020-07-13 17:59:52 -07:00
guard !self.isStarted else { return finish(nil) }
2023-03-01 00:48:36 -05:00
// Quit any other running AltStore processes to prevent concurrent database access during and after migration.
self.ignoreWillAccessDatabaseNotification = true
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .willAccessDatabase, nil, nil, true)
2023-03-01 00:48:36 -05:00
self.migrateDatabaseToAppGroupIfNeeded { result in
switch result {
case let .failure(error): finish(error)
case .success:
2023-03-01 00:48:36 -05:00
self.persistentContainer.loadPersistentStores { _, error in
guard error == nil else { return finish(error!) }
2023-03-01 00:48:36 -05:00
self.prepareDatabase { result in
switch result {
case let .failure(error): finish(error)
case .success: finish(nil)
}
}
2020-07-13 17:59:52 -07:00
}
}
}
2019-05-20 21:24:53 +02:00
}
}
2023-03-01 00:48:36 -05:00
func signOut(completionHandler: @escaping (Error?) -> Void) {
persistentContainer.performBackgroundTask { context in
if let account = self.activeAccount(in: context) {
2019-06-06 14:46:23 -07:00
account.isActiveAccount = false
}
2023-03-01 00:48:36 -05:00
if let team = self.activeTeam(in: context) {
2019-06-06 14:46:23 -07:00
team.isActiveTeam = false
}
2023-03-01 00:48:36 -05:00
do {
2019-06-06 14:46:23 -07:00
try context.save()
2023-03-01 00:48:36 -05:00
2019-06-06 14:46:23 -07:00
Keychain.shared.reset()
2023-03-01 00:48:36 -05:00
2019-06-06 14:46:23 -07:00
completionHandler(nil)
2023-03-01 00:48:36 -05:00
} catch {
2019-06-06 14:46:23 -07:00
print("Failed to save when signing out.", error)
completionHandler(error)
}
}
}
2023-03-01 00:48:36 -05:00
func purgeLoggedErrors(before date: Date? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
persistentContainer.performBackgroundTask { context in
do {
let predicate = date.map { NSPredicate(format: "%K <= %@", #keyPath(LoggedError.date), $0 as NSDate) }
2023-03-01 00:48:36 -05:00
let loggedErrors = LoggedError.all(satisfying: predicate, in: context, requestProperties: [\.returnsObjectsAsFaults: true])
loggedErrors.forEach { context.delete($0) }
2023-03-01 00:48:36 -05:00
try context.save()
2023-03-01 00:48:36 -05:00
completion(.success(()))
2023-03-01 00:48:36 -05:00
} catch {
completion(.failure(error))
}
}
}
2019-05-20 21:24:53 +02:00
}
2023-03-01 00:48:36 -05:00
public extension DatabaseManager {
2019-05-20 21:24:53 +02:00
var viewContext: NSManagedObjectContext {
2023-03-01 00:48:36 -05:00
persistentContainer.viewContext
2019-05-20 21:24:53 +02:00
}
2023-03-01 00:48:36 -05:00
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account? {
2019-06-06 14:46:23 -07:00
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
2023-03-01 00:48:36 -05:00
2019-06-06 14:46:23 -07:00
let activeAccount = Account.first(satisfying: predicate, in: context)
return activeAccount
}
2023-03-01 00:48:36 -05:00
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team? {
2019-06-06 14:46:23 -07:00
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
2023-03-01 00:48:36 -05:00
2019-06-06 14:46:23 -07:00
let activeTeam = Team.first(satisfying: predicate, in: context)
return activeTeam
}
2023-03-01 00:48:36 -05:00
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount? {
guard let patreonAccountID = Keychain.shared.patreonAccountID else { return nil }
2023-03-01 00:48:36 -05:00
let predicate = NSPredicate(format: "%K == %@", #keyPath(PatreonAccount.identifier), patreonAccountID)
2023-03-01 00:48:36 -05:00
let patreonAccount = PatreonAccount.first(satisfying: predicate, in: context)
return patreonAccount
}
2019-06-06 14:46:23 -07:00
}
2023-03-01 00:48:36 -05:00
private extension DatabaseManager {
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void) {
guard !Bundle.isAppExtension else { return completionHandler(.success(())) }
let context = persistentContainer.newBackgroundContext()
context.performAndWait {
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
2023-03-01 00:48:36 -05:00
let altStoreSource: Source
2023-03-01 00:48:36 -05:00
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) {
altStoreSource = source
2023-03-01 00:48:36 -05:00
} else {
altStoreSource = Source.makeAltStoreSource(in: context)
}
2023-03-01 00:48:36 -05:00
// Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL
2023-03-01 00:48:36 -05:00
2019-07-31 14:07:00 -07:00
let storeApp: StoreApp
2023-03-01 00:48:36 -05:00
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context) {
storeApp = app
2023-03-01 00:48:36 -05:00
} else {
2019-07-31 14:07:00 -07:00
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.latestVersion?.version = localApp.version
storeApp.source = altStoreSource
}
2023-03-01 00:48:36 -05:00
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp
2023-03-01 00:48:36 -05:00
if let app = storeApp.installedApp {
installedApp = app
2023-03-01 00:48:36 -05:00
} else {
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp
}
2023-03-01 00:48:36 -05:00
/* App Extensions */
var installedExtensions = Set<InstalledExtension>()
2023-03-01 00:48:36 -05:00
for appExtension in localApp.appExtensions {
let resignedBundleID = appExtension.bundleIdentifier
let originalBundleID = resignedBundleID.replacingOccurrences(of: localApp.bundleIdentifier, with: StoreApp.altstoreAppID)
2023-03-01 00:48:36 -05:00
let installedExtension: InstalledExtension
2023-03-01 00:48:36 -05:00
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID }) {
installedExtension = appExtension
2023-03-01 00:48:36 -05:00
} else {
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: context)
}
2023-03-01 00:48:36 -05:00
installedExtension.update(resignedAppExtension: appExtension)
2023-03-01 00:48:36 -05:00
installedExtensions.insert(installedExtension)
}
2023-03-01 00:48:36 -05:00
installedApp.appExtensions = installedExtensions
2023-03-01 00:48:36 -05:00
let fileURL = installedApp.fileURL
2023-03-01 00:48:36 -05:00
#if DEBUG
2023-03-01 00:48:36 -05:00
let replaceCachedApp = true
#else
2023-03-01 00:48:36 -05:00
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
#endif
2023-03-01 00:48:36 -05:00
if replaceCachedApp {
func update(_ bundle: Bundle, bundleID: String) throws {
let infoPlistURL = bundle.bundleURL.appendingPathComponent("Info.plist")
2023-03-01 00:48:36 -05:00
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = bundleID
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
}
2023-03-01 00:48:36 -05:00
FileManager.default.prepareTemporaryURL { temporaryFileURL in
do {
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
2023-03-01 00:48:36 -05:00
guard let appBundle = Bundle(url: temporaryFileURL) else { throw ALTError(.invalidApp) }
try update(appBundle, bundleID: StoreApp.altstoreAppID)
2023-03-01 00:48:36 -05:00
if let tempApp = ALTApplication(fileURL: temporaryFileURL) {
for appExtension in tempApp.appExtensions {
guard let extensionBundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.invalidApp) }
guard let installedExtension = installedExtensions.first(where: { $0.resignedBundleIdentifier == appExtension.bundleIdentifier }) else { throw ALTError(.invalidApp) }
try update(extensionBundle, bundleID: installedExtension.bundleIdentifier)
}
}
2023-03-01 00:48:36 -05:00
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
2023-03-01 00:48:36 -05:00
} catch {
print("Failed to copy AltStore app bundle to its proper location.", error)
}
}
}
2023-03-01 00:48:36 -05:00
let cachedRefreshedDate = installedApp.refreshedDate
let cachedExpirationDate = installedApp.expirationDate
2023-03-01 00:48:36 -05:00
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
2023-03-01 00:48:36 -05:00
if installedApp.refreshedDate < cachedRefreshedDate {
// Embedded provisioning profile has a creation date older than our refreshed date.
// This most likely means we've refreshed the app since then, and profile is now outdated,
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
2023-03-01 00:48:36 -05:00
installedApp.refreshedDate = cachedRefreshedDate
installedApp.expirationDate = cachedExpirationDate
}
2023-03-01 00:48:36 -05:00
do {
try context.save()
completionHandler(.success(()))
2023-03-01 00:48:36 -05:00
} catch {
completionHandler(.failure(error))
}
}
}
2023-03-01 00:48:36 -05:00
func migrateDatabaseToAppGroupIfNeeded(completion: @escaping (Result<Void, Error>) -> Void) {
guard UserDefaults.shared.requiresAppGroupMigration else { return completion(.success(())) }
2023-03-01 00:48:36 -05:00
func finish(_ result: Result<Void, Error>) {
switch result {
case let .failure(error): completion(.failure(error))
case .success:
UserDefaults.shared.requiresAppGroupMigration = false
completion(.success(()))
}
}
2023-03-01 00:48:36 -05:00
let previousDatabaseURL = PersistentContainer.legacyDirectoryURL().appendingPathComponent("AltStore.sqlite")
let databaseURL = PersistentContainer.defaultDirectoryURL().appendingPathComponent("AltStore.sqlite")
2023-03-01 00:48:36 -05:00
let previousAppsDirectoryURL = InstalledApp.legacyAppsDirectoryURL
let appsDirectoryURL = InstalledApp.appsDirectoryURL
2023-03-01 00:48:36 -05:00
let databaseIntent = NSFileAccessIntent.writingIntent(with: databaseURL, options: [.forReplacing])
let appsIntent = NSFileAccessIntent.writingIntent(with: appsDirectoryURL, options: [.forReplacing])
2023-03-01 00:48:36 -05:00
coordinator.coordinate(with: [databaseIntent, appsIntent], queue: coordinatorQueue) { error in
do {
if let error = error {
throw error
}
2023-03-01 00:48:36 -05:00
let description = NSPersistentStoreDescription(url: previousDatabaseURL)
2023-03-01 00:48:36 -05:00
// Disable WAL to remove extra files automatically during migration.
description.setOption(["journal_mode": "DELETE"] as NSDictionary, forKey: NSSQLitePragmasOption)
2023-03-01 00:48:36 -05:00
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.persistentContainer.managedObjectModel)
2023-03-01 00:48:36 -05:00
// Migrate database
2023-03-01 00:48:36 -05:00
if FileManager.default.fileExists(atPath: previousDatabaseURL.path) {
if FileManager.default.fileExists(atPath: databaseURL.path, isDirectory: nil) {
try FileManager.default.removeItem(at: databaseURL)
}
2023-03-01 00:48:36 -05:00
let previousDatabase = try persistentStoreCoordinator.addPersistentStore(ofType: description.type, configurationName: description.configuration, at: description.url, options: description.options)
2023-03-01 00:48:36 -05:00
// Pass nil options to prevent later error due to self.persistentContainer using WAL.
try persistentStoreCoordinator.migratePersistentStore(previousDatabase, to: databaseURL, options: nil, withType: NSSQLiteStoreType)
2023-03-01 00:48:36 -05:00
try FileManager.default.removeItem(at: previousDatabaseURL)
}
2023-03-01 00:48:36 -05:00
// Migrate apps
2023-03-01 00:48:36 -05:00
if FileManager.default.fileExists(atPath: previousAppsDirectoryURL.path, isDirectory: nil) {
_ = try FileManager.default.replaceItemAt(appsDirectoryURL, withItemAt: previousAppsDirectoryURL)
}
2023-03-01 00:48:36 -05:00
finish(.success(()))
2023-03-01 00:48:36 -05:00
} catch {
print("Failed to migrate database to app group:", error)
finish(.failure(error))
}
}
}
2023-03-01 00:48:36 -05:00
func receivedWillAccessDatabaseNotification() {
defer { self.ignoreWillAccessDatabaseNotification = false }
2023-03-01 00:48:36 -05:00
// Ignore notifications sent by the current process.
2023-03-01 00:48:36 -05:00
guard !ignoreWillAccessDatabaseNotification else { return }
exit(104)
}
}