From db87d9ca7bc8b237e80fb9fb8ccb0790b422c00e Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 5 Jun 2020 14:35:05 -0700 Subject: [PATCH] [AltDaemon] Synchronizes AppManager operations Installing and removing apps is now done on a serial dispatch queue, and installing/removing profiles uses file coordination. --- AltDaemon/AppManager.swift | 134 ++++++++++++++++++++++----------- AltDaemon/RequestHandler.swift | 84 +++++++++++---------- 2 files changed, 135 insertions(+), 83 deletions(-) diff --git a/AltDaemon/AppManager.swift b/AltDaemon/AppManager.swift index 743215c8..50d5bb3f 100644 --- a/AltDaemon/AppManager.swift +++ b/AltDaemon/AppManager.swift @@ -19,63 +19,107 @@ struct AppManager { static let shared = AppManager() + private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated) + private let profilesQueue = OperationQueue() + + private let fileCoordinator = NSFileCoordinator() + private init() { + self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue" + self.profilesQueue.qualityOfService = .userInitiated } - func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set?) throws + func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set?, completionHandler: @escaping (Result) -> Void) { - let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) - - let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any] - try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) - } - - func removeApp(forBundleIdentifier bundleIdentifier: String) - { - let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) - lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil) - } - - func install(_ profiles: Set, activeProfiles: Set?) throws - { - let installingBundleIDs = Set(profiles.map(\.bundleIdentifier)) - - let profileURLs = try FileManager.default.contentsOfDirectory(at: .profilesDirectoryURL, includingPropertiesForKeys: nil, options: []) - - // Remove all inactive profiles (if active profiles are provided), and the previous profiles. - for fileURL in profileURLs - { - guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } + self.appQueue.async { + let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) - if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) - { - try FileManager.default.removeItem(at: fileURL) - } - else - { - print("Ignoring:", profile.bundleIdentifier, profile.uuid) - } - } - - for profile in profiles - { - let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased()) - try profile.data.write(to: destinationURL, options: .atomic) + let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any] + let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) } + + completionHandler(result) } } - func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set) throws + func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result) -> Void) { - let profileURLs = try FileManager.default.contentsOfDirectory(at: .profilesDirectoryURL, includingPropertiesForKeys: nil, options: []) - - for fileURL in profileURLs - { - guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } + self.appQueue.async { + let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self) + lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil) - if bundleIdentifiers.contains(profile.bundleIdentifier) + completionHandler(.success(())) + } + } + + func install(_ profiles: Set, activeProfiles: Set?, completionHandler: @escaping (Result) -> Void) + { + let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) + self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in + do { - try FileManager.default.removeItem(at: fileURL) + if let error = error + { + throw error + } + + let installingBundleIDs = Set(profiles.map(\.bundleIdentifier)) + + let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) + + // Remove all inactive profiles (if active profiles are provided), and the previous profiles. + for fileURL in profileURLs + { + guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } + + if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile) + { + try FileManager.default.removeItem(at: fileURL) + } + else + { + print("Ignoring:", profile.bundleIdentifier, profile.uuid) + } + } + + for profile in profiles + { + let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased()) + try profile.data.write(to: destinationURL, options: .atomic) + } + + completionHandler(.success(())) + } + catch + { + completionHandler(.failure(error)) + } + } + } + + func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set, completionHandler: @escaping (Result) -> Void) + { + let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: []) + self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in + do + { + let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: []) + + for fileURL in profileURLs + { + guard let profile = ALTProvisioningProfile(url: fileURL) else { continue } + + if bundleIdentifiers.contains(profile.bundleIdentifier) + { + try FileManager.default.removeItem(at: fileURL) + } + } + + completionHandler(.success(())) + } + catch + { + completionHandler(.failure(error)) } } } diff --git a/AltDaemon/RequestHandler.swift b/AltDaemon/RequestHandler.swift index a9138f85..aff9e5d1 100644 --- a/AltDaemon/RequestHandler.swift +++ b/AltDaemon/RequestHandler.swift @@ -52,12 +52,12 @@ struct RequestHandler: AltKit.RequestHandler guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) } guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) } - try AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) - - print("Installed app with result:", result) - - let response = InstallationProgressResponse(progress: 1.0) - completionHandler(.success(response)) + AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in + let result = result.map { InstallationProgressResponse(progress: 1.0) } + print("Installed app with result:", result) + + completionHandler(result) + } } catch { @@ -69,48 +69,56 @@ struct RequestHandler: AltKit.RequestHandler func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection, completionHandler: @escaping (Result) -> Void) { - do - { - try AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) - - print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier }) - - let response = InstallProvisioningProfilesResponse() - completionHandler(.success(response)) - } - catch - { - print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error) - completionHandler(.failure(error)) + AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in + switch result + { + case .failure(let error): + print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error) + completionHandler(.failure(error)) + + case .success: + print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier }) + + let response = InstallProvisioningProfilesResponse() + completionHandler(.success(response)) + } } } func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection, completionHandler: @escaping (Result) -> Void) { - do - { - try AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) - - print("Removed profiles:", request.bundleIdentifiers) - - let response = RemoveProvisioningProfilesResponse() - completionHandler(.success(response)) - } - catch - { - print("Failed to remove profiles \(request.bundleIdentifiers):", error) - completionHandler(.failure(error)) + AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in + switch result + { + case .failure(let error): + print("Failed to remove profiles \(request.bundleIdentifiers):", error) + completionHandler(.failure(error)) + + case .success: + print("Removed profiles:", request.bundleIdentifiers) + + let response = RemoveProvisioningProfilesResponse() + completionHandler(.success(response)) + } } } func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result) -> Void) { - AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) - - print("Removed app:", request.bundleIdentifier) - - let response = RemoveAppResponse() - completionHandler(.success(response)) + AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in + switch result + { + case .failure(let error): + print("Failed to remove app \(request.bundleIdentifier):", error) + completionHandler(.failure(error)) + + case .success: + print("Removed app:", request.bundleIdentifier) + + let response = RemoveAppResponse() + completionHandler(.success(response)) + } + } } }