Prioritizes app refresh order

Tries to refresh apps that are about to expire first, and then always refreshes AltStore itself last, since refreshing AltStore means that the app will quit.
This commit is contained in:
Riley Testut
2019-06-21 11:20:03 -07:00
parent c096fd02b4
commit 39c84e623a
21 changed files with 1016 additions and 621 deletions

View File

@@ -13,8 +13,17 @@ public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
public struct ServerRequest: Codable
protocol ServerMessage: Codable
{
var version: Int { get }
var identifier: String { get }
}
public struct PrepareAppRequest: ServerMessage
{
public var version = 1
public var identifier = "PrepareApp"
public var udid: String
public var contentSize: Int
@@ -25,8 +34,21 @@ public struct ServerRequest: Codable
}
}
public struct ServerResponse: Codable
public struct BeginInstallationRequest: ServerMessage
{
public var version = 1
public var identifier = "BeginInstallation"
public init()
{
}
}
public struct ServerResponse: ServerMessage
{
public var version = 1
public var identifier = "ServerResponse"
public var progress: Double
public var error: ALTServerError? {

View File

@@ -173,25 +173,6 @@ private extension ConnectionManager
guard !self.connections.contains(where: { $0 === connection }) else { return }
self.connections.append(connection)
func finish(error: ALTServerError?)
{
if let error = error
{
print("Failed to process request from \(connection.endpoint).", error)
}
else
{
print("Processed request from \(connection.endpoint).")
}
let response = ServerResponse(progress: 1.0, error: error)
self.send(response, to: connection) { (result) in
print("Sent response to \(connection.endpoint) with result:", result)
self.disconnect(connection)
}
}
connection.stateUpdateHandler = { [weak self] (state) in
switch state
@@ -201,24 +182,8 @@ private extension ConnectionManager
case .ready:
print("Connected to client:", connection.endpoint)
self?.receiveRequest(from: connection) { (result) in
print("Received request with result:", result)
switch result
{
case .failure(let error): finish(error: error)
case .success(let request, let fileURL):
print("Installing to device \(request.udid)...")
self?.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
case .failure(let error): finish(error: error)
case .success: finish(error: nil)
}
}
}
self?.receiveApp(from: connection) { (result) in
self?.finish(connection: connection, error: result.error)
}
case .waiting:
@@ -238,44 +203,70 @@ private extension ConnectionManager
connection.start(queue: self.dispatchQueue)
}
func receiveRequest(from connection: NWConnection, completionHandler: @escaping (Result<(ServerRequest, URL), ALTServerError>) -> Void)
func receiveApp(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
print("Receiving request size")
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
self.receive(PrepareAppRequest.self, from: connection) { (result) in
print("Received request with result:", result)
switch result
{
let data = try self.process(data: data, error: error, from: connection)
print("Receiving request")
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
case .failure(let error): completionHandler(.failure(error))
case .success(let request):
self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result)
switch result
{
let data = try self.process(data: data, error: error, from: connection)
case .failure(let error): completionHandler(.failure(error))
case .success(let request, let fileURL):
print("Awaiting begin installation request...")
let request = try JSONDecoder().decode(ServerRequest.self, from: data)
print("Receiving app data (Size: \(request.contentSize))")
self.process(request, from: connection, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(ALTServerError(error)))
self.receive(BeginInstallationRequest.self, from: connection) { (result) in
print("Received begin installation request with result:", result)
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
print("Installing to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success: completionHandler(.success(()))
}
}
}
}
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func process(_ request: ServerRequest, from connection: NWConnection, completionHandler: @escaping (Result<(ServerRequest, URL), ALTServerError>) -> Void)
func finish(connection: NWConnection, error: ALTServerError?)
{
if let error = error
{
print("Failed to process request from \(connection.endpoint).", error)
}
else
{
print("Processed request from \(connection.endpoint).")
}
let response = ServerResponse(progress: 1.0, error: error)
self.send(response, to: connection) { (result) in
print("Sent response to \(connection.endpoint) with result:", result)
self.disconnect(connection)
}
}
func receiveApp(for request: PrepareAppRequest, from connection: NWConnection, completionHandler: @escaping (Result<(PrepareAppRequest, URL), ALTServerError>) -> Void)
{
connection.receive(minimumIncompleteLength: request.contentSize, maximumLength: request.contentSize) { (data, _, _, error) in
do
@@ -346,10 +337,10 @@ private extension ConnectionManager
})
}
func send(_ response: ServerResponse, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
func send<T: Encodable>(_ response: T, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
do
{
{
let data = try JSONEncoder().encode(response)
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
@@ -383,4 +374,41 @@ private extension ConnectionManager
completionHandler(.failure(.init(.invalidResponse)))
}
}
func receive<T: Decodable>(_ responseType: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
print("Receiving request size")
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
print("Receiving request...")
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let request = try JSONDecoder().decode(T.self, from: data)
print("Received installation request:", request)
completionHandler(.success(request))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
}

View File

@@ -90,6 +90,10 @@
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; };
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; };
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */; };
BF9B63C6229DD44E002F0A62 /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
@@ -150,7 +154,7 @@
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; };
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; };
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */; };
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; };
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; };
@@ -311,6 +315,10 @@
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; };
BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; };
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppsOperation.swift; sourceTree = "<group>"; };
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
@@ -374,7 +382,7 @@
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = "<group>"; };
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = "<group>"; };
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
@@ -651,6 +659,7 @@
isa = PBXGroup;
children = (
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
BF770E5522BC3C02002A40FE /* Server.swift */,
);
path = Server;
sourceTree = "<group>";
@@ -808,10 +817,13 @@
children = (
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */,
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */,
BF770E5322BC044E002A40FE /* AppOperationContext.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */,
);
path = Operations;
@@ -1171,7 +1183,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */,
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */,
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
@@ -1184,6 +1196,7 @@
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */,
@@ -1196,12 +1209,15 @@
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */,
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -63,7 +63,7 @@ extension AppDelegate
{
private func prepareForBackgroundFetch()
{
// Fetch every 6 hours.
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
@@ -72,49 +72,100 @@ extension AppDelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: DatabaseManager.shared.viewContext)
guard !installedApps.isEmpty else { return completionHandler(.noData) }
print("Apps to refresh:", installedApps.map { $0.app.identifier })
ServerManager.shared.startDiscovering()
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Refreshing apps...", comment: "")
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error)
}
}
// Wait a few seconds so we have a chance to discover nearby AltServers.
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
_ = AppManager.shared.refresh(installedApps, presentingViewController: nil) { (result) in
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
{
ServerManager.shared.stopDiscovering()
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
do
{
let results = try result.get()
shouldPresentAlert = !results.isEmpty
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
content.title = "Refreshed Apps!"
content.body = "Successfully refreshed all apps."
completionHandler(.newData)
content.title = NSLocalizedString("Refreshed all apps!", comment: "")
}
catch
{
print("Failed to refresh apps in background.", error)
content.title = "Failed to Refresh Apps"
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
completionHandler(.failed)
shouldPresentAlert = true
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "RefreshedApps", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error)
if shouldPresentAlert
{
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error)
}
}
}
switch result
{
case .failure(ConnectionError.serverNotFound): completionHandler(.newData)
case .failure: completionHandler(.failed)
case .success: completionHandler(.newData)
}
}
let group = AppManager.shared.refresh(installedApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
guard installedApp.app.identifier == App.altstoreAppID else { return }
// We're starting to install AltStore, which means the app is about to quit.
// So, we say we were successful even though we technically don't know 100% yet.
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
if let error = group.error
{
finish(.failure(error))
}
else
{
var results = group.results
results[installedApp.app.identifier] = .success(installedApp)
finish(.success(results))
}
}
group.completionHandler = { (result) in
finish(result)
}
}
}

View File

@@ -127,14 +127,11 @@ private extension AppDetailViewController
let progressView = UIProgressView(progressViewStyle: .bar)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.progress = 0.0
let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
do
{
let installedApp = try result.get()
do { try installedApp.managedObjectContext?.save() }
catch { print("Failed to save context.", error) }
_ = try result.get()
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Installed \(self.app.name)!", detailText: nil)
@@ -154,7 +151,7 @@ private extension AppDetailViewController
toastView.show(in: self.navigationController!.view, duration: 2)
}
}
DispatchQueue.main.async {
UIView.animate(withDuration: 0.4, animations: {
progressView.alpha = 0.0

View File

@@ -24,10 +24,11 @@ class AppManager
static let shared = AppManager()
private let operationQueue = OperationQueue()
private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue")
private init()
{
self.operationQueue.name = "com.rileytestut.AltStore.AppManager"
self.operationQueue.name = "com.altstore.AppManager.operationQueue"
}
}
@@ -104,7 +105,8 @@ extension AppManager
{
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = self.install([app], forceDownload: true, presentingViewController: presentingViewController) { (result) in
let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController)
group.completionHandler = { (result) in
do
{
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
@@ -116,170 +118,179 @@ extension AppManager
}
}
return progress
return group.progress
}
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
return self.refresh([app], presentingViewController: presentingViewController) { (result) in
do
{
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
completionHandler(result)
}
catch
{
completionHandler(.failure(error))
}
}
}
@discardableResult func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
{
let apps = installedApps.compactMap { $0.app }
let progress = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, completionHandler: completionHandler)
return progress
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
return group
}
}
private extension AppManager
{
func install(_ apps: [App], forceDownload: Bool, presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
func install(_ apps: [App], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
{
let progress = Progress.discreteProgress(totalUnitCount: Int64(apps.count))
// Authenticate -> Download (if necessary) -> Resign -> Send -> Install.
let group = group ?? OperationGroup()
guard let context = apps.first?.managedObjectContext else {
completionHandler(.success([:]))
return progress
guard let server = ServerManager.shared.discoveredServers.first else {
DispatchQueue.main.async {
group.completionHandler?(.failure(ConnectionError.serverNotFound))
}
return group
}
// Authenticate
group.server = server
var operations = [Operation]()
/* Authenticate */
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let signer):
// Download
context.perform {
let dispatchGroup = DispatchGroup()
var results = [String: Result<InstalledApp, Error>]()
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
for app in apps
{
let appProgress = Progress(totalUnitCount: 100)
let appID = app.identifier
print("Installing app:", appID)
dispatchGroup.enter()
func finishApp(_ result: Result<InstalledApp, Error>)
{
switch result
{
case .failure(let error): print("Failed to install app \(appID).", error)
case .success: print("Installed app:", appID)
}
results[appID] = result
dispatchGroup.leave()
}
// Ensure app is downloaded.
let downloadAppOperation = DownloadAppOperation(app: app)
downloadAppOperation.useCachedAppIfAvailable = !forceDownload
downloadAppOperation.context = backgroundContext
downloadAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
finishApp(.failure(error))
case .success(let installedApp):
// Refresh
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
finishApp(result)
}
if forceDownload
{
appProgress.addChild(resignProgress, withPendingUnitCount: 10)
appProgress.addChild(installProgress, withPendingUnitCount: 50)
}
else
{
appProgress.addChild(resignProgress, withPendingUnitCount: 20)
appProgress.addChild(installProgress, withPendingUnitCount: 80)
}
}
}
if forceDownload
{
appProgress.addChild(downloadAppOperation.progress, withPendingUnitCount: 40)
}
progress.addChild(appProgress, withPendingUnitCount: 1)
self.operationQueue.addOperation(downloadAppOperation)
}
dispatchGroup.notify(queue: .global()) {
backgroundContext.perform {
completionHandler(.success(results))
}
}
}
case .failure(let error): group.error = error
case .success(let signer): group.signer = signer
}
}
operations.append(authenticationOperation)
self.operationQueue.addOperation(authenticationOperation)
return progress
for app in apps
{
let context = AppOperationContext(appIdentifier: app.identifier, group: group)
let progress = Progress.discreteProgress(totalUnitCount: 100)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let fileURL): context.resignedFileURL = fileURL
}
}
resignAppOperation.addDependency(authenticationOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation)
/* Download */
let fileURL = InstalledApp.fileURL(for: app)
if let installedApp = app.installedApp, FileManager.default.fileExists(atPath: fileURL.path), !forceDownload
{
// Already installed, don't need to download.
// If we don't need to download the app, reduce the total unit count by 40.
progress.totalUnitCount -= 40
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
backgroundContext.performAndWait {
let installedApp = backgroundContext.object(with: installedApp.objectID) as! InstalledApp
context.installedApp = installedApp
}
}
else
{
// App is not yet installed (or we're forcing it to download a new version), so download it before resigning it.
let downloadOperation = DownloadAppOperation(app: app)
downloadOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let installedApp): context.installedApp = installedApp
}
}
progress.addChild(downloadOperation.progress, withPendingUnitCount: 40)
resignAppOperation.addDependency(downloadOperation)
operations.append(downloadOperation)
}
/* Send */
let sendAppOperation = SendAppOperation(context: context)
sendAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let connection): context.connection = connection
}
}
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
sendAppOperation.addDependency(resignAppOperation)
operations.append(sendAppOperation)
/* Install */
let installOperation = InstallAppOperation(context: context)
installOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success: break
}
self.finishAppOperation(context)
}
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation)
operations.append(installOperation)
group.progress.totalUnitCount += 1
group.progress.addChild(progress, withPendingUnitCount: 1)
}
group.addOperations(operations)
return group
}
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> (Progress, Progress)
@discardableResult func process<T>(_ result: Result<T, Error>, context: AppOperationContext) -> T?
{
let context = installedApp.managedObjectContext
let resignAppOperation = ResignAppOperation(installedApp: installedApp)
let installAppOperation = InstallAppOperation()
// Resign
resignAppOperation.signer = signer
resignAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
installAppOperation.cancel()
completionHandler(.failure(error))
case .success(let resignedURL):
installAppOperation.fileURL = resignedURL
}
do
{
let value = try result.get()
return value
}
// Install
installAppOperation.addDependency(resignAppOperation)
installAppOperation.resultHandler = { (result) in
switch result
catch
{
context.error = error
return nil
}
}
func finishAppOperation(_ context: AppOperationContext)
{
self.processingQueue.sync {
if let error = context.error
{
case .failure(let error): completionHandler(.failure(error))
case .success:
context?.perform {
completionHandler(.success(installedApp))
context.group.results[context.appIdentifier] = .failure(error)
}
else if let installedApp = context.installedApp
{
context.group.results[context.appIdentifier] = .success(installedApp)
// Save after each installation.
installedApp.managedObjectContext?.perform {
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
}
print("Finished operation!", context.appIdentifier)
if context.group.results.count == context.group.progress.totalUnitCount
{
context.group.completionHandler?(.success(context.group.results))
}
}
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
return (resignAppOperation.progress, installAppOperation.progress)
}
}

View File

@@ -34,6 +34,7 @@
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
<uniquenessConstraints>
@@ -57,7 +58,7 @@
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="210"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="105"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
</elements>
</model>

View File

@@ -8,6 +8,7 @@
import CoreData
import AltSign
import Roxas
public class DatabaseManager
@@ -120,14 +121,23 @@ private extension DatabaseManager
altStoreApp.version = version
}
if let installedApp = altStoreApp.installedApp
let installedApp: InstalledApp
if let app = altStoreApp.installedApp
{
installedApp.version = version
installedApp = app
}
else
{
let installedApp = InstalledApp(app: altStoreApp, bundleIdentifier: altStoreApp.identifier, expirationDate: Date(), context: context)
installedApp.version = version
installedApp = InstalledApp(app: altStoreApp, bundleIdentifier: altStoreApp.identifier, context: context)
}
installedApp.version = version
if let provisioningProfileURL = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision"), let provisioningProfile = ALTProvisioningProfile(url: provisioningProfileURL)
{
installedApp.refreshedDate = provisioningProfile.creationDate
installedApp.expirationDate = provisioningProfile.expirationDate
}
do

View File

@@ -16,6 +16,7 @@ class InstalledApp: NSManagedObject, Fetchable
@NSManaged var bundleIdentifier: String
@NSManaged var version: String
@NSManaged var refreshedDate: Date
@NSManaged var expirationDate: Date
/* Relationships */
@@ -26,7 +27,7 @@ class InstalledApp: NSManagedObject, Fetchable
super.init(entity: entity, insertInto: context)
}
init(app: App, bundleIdentifier: String, expirationDate: Date, context: NSManagedObjectContext)
init(app: App, bundleIdentifier: String, context: NSManagedObjectContext)
{
super.init(entity: InstalledApp.entity(), insertInto: context)
@@ -35,7 +36,9 @@ class InstalledApp: NSManagedObject, Fetchable
self.version = app.version
self.bundleIdentifier = bundleIdentifier
self.expirationDate = expirationDate
self.refreshedDate = Date()
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
}
}
@@ -45,6 +48,52 @@ extension InstalledApp
{
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
}
class func fetchAltStore(in context: NSManagedObjectContext) -> InstalledApp?
{
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.app.identifier), App.altstoreAppID)
let altStore = InstalledApp.first(satisfying: predicate, in: context)
return altStore
}
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
{
let predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.app.identifier), App.altstoreAppID)
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
in: context)
if let altStoreApp = InstalledApp.fetchAltStore(in: context)
{
// Refresh AltStore last since it causes app to quit.
installedApps.append(altStoreApp)
}
return installedApps
}
class func fetchAppsForBackgroundRefresh(in context: NSManagedObjectContext) -> [InstalledApp]
{
let date = Date().addingTimeInterval(-120)
let predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
#keyPath(InstalledApp.refreshedDate), date as NSDate,
#keyPath(InstalledApp.app.identifier), App.altstoreAppID)
var installedApps = InstalledApp.all(satisfying: predicate,
sortedBy: [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true)],
in: context)
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
{
// Refresh AltStore last since it causes app to quit.
installedApps.append(altStoreApp)
}
return installedApps
}
}
extension InstalledApp
@@ -67,10 +116,10 @@ extension InstalledApp
return appsDirectoryURL
}
class func ipaURL(for app: App) -> URL
class func fileURL(for app: App) -> URL
{
let ipaURL = self.directoryURL(for: app).appendingPathComponent("App.ipa")
return ipaURL
let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
return appURL
}
class func refreshedIPAURL(for app: App) -> URL
@@ -79,7 +128,6 @@ extension InstalledApp
return ipaURL
}
class func directoryURL(for app: App) -> URL
{
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
@@ -94,8 +142,8 @@ extension InstalledApp
return InstalledApp.directoryURL(for: self.app)
}
var ipaURL: URL {
return InstalledApp.ipaURL(for: self.app)
var fileURL: URL {
return InstalledApp.fileURL(for: self.app)
}
var refreshedIPAURL: URL {

View File

@@ -24,6 +24,8 @@ class MyAppsViewController: UITableViewController
return dateFormatter
}()
private var refreshGroup: OperationGroup?
@IBOutlet private var progressView: UIProgressView!
override func viewDidLoad()
@@ -119,9 +121,25 @@ private extension MyAppsViewController
{
sender.isIndicatingActivity = true
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
let installedApps = InstalledApp.fetchAppsForRefreshingAll(in: DatabaseManager.shared.viewContext)
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
self.refresh(installedApps) { (result) in
sender.isIndicatingActivity = false
}
}
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void)
{
if self.refreshGroup == nil
{
let toastView = RSTToastView(text: "Refreshing...", detailText: nil)
toastView.tintColor = .altPurple
toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.navigationController?.view ?? self.view)
}
let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup)
group.completionHandler = { (result) in
DispatchQueue.main.async {
switch result
{
@@ -164,46 +182,16 @@ private extension MyAppsViewController
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
sender.isIndicatingActivity = false
self.update()
}
}
self.progressView.observedProgress = progress
}
func refresh(_ installedApp: InstalledApp)
{
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
self.refreshGroup = nil
completionHandler(result)
}
}
self.progressView.observedProgress = progress
self.progressView.observedProgress = group.progress
self.refreshGroup = group
}
}
@@ -231,42 +219,9 @@ extension MyAppsViewController
let refreshAction = UITableViewRowAction(style: .normal, title: "Refresh") { (action, indexPath) in
let installedApp = self.dataSource.item(at: indexPath)
let toastView = RSTToastView(text: "Refreshing...", detailText: nil)
toastView.tintColor = .altPurple
toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.navigationController?.view ?? self.view)
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
self.update()
}
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altPurple
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
}
}
DispatchQueue.main.async {
self.progressView.observedProgress = nil
self.progressView.progress = 0.0
}
self.refresh([installedApp]) { (result) in
print("Refreshed", installedApp.app.identifier)
}
self.progressView.observedProgress = progress
}
return [deleteAction, refreshAction]

View File

@@ -0,0 +1,35 @@
//
// Contexts.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Network
class AppOperationContext
{
var appIdentifier: String
var group: OperationGroup
var installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var resignedFileURL: URL?
var connection: NWConnection?
var error: Error?
init(appIdentifier: String, group: OperationGroup)
{
self.appIdentifier = appIdentifier
self.group = group
}
}

View File

@@ -19,16 +19,18 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
var useCachedAppIfAvailable = false
lazy var context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
private let downloadURL: URL
private let ipaURL: URL
private let appIdentifier: String
private let sourceURL: URL
private let destinationURL: URL
private let session = URLSession(configuration: .default)
init(app: App)
{
self.app = app
self.downloadURL = app.downloadURL
self.ipaURL = InstalledApp.ipaURL(for: app)
self.appIdentifier = app.identifier
self.sourceURL = app.downloadURL
self.destinationURL = InstalledApp.fileURL(for: app)
super.init()
@@ -39,14 +41,36 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
{
super.main()
func finish(error: Error?)
print("Downloading App:", self.appIdentifier)
func finishOperation(_ result: Result<URL, Error>)
{
if let error = error
{
self.finish(.failure(error))
}
else
do
{
let fileURL = try result.get()
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound }
if isDirectory.boolValue
{
// Directory, so assuming this is .app bundle.
guard Bundle(url: fileURL) != nil else { throw OperationError.invalidApp }
try FileManager.default.copyItem(at: fileURL, to: self.destinationURL, shouldReplace: true)
}
else
{
// File, so assuming this is a .ipa file.
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
let bundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
try FileManager.default.copyItem(at: bundleURL, to: self.destinationURL, shouldReplace: true)
}
self.context.perform {
let app = self.context.object(with: self.app.objectID) as! App
@@ -59,40 +83,41 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
}
else
{
installedApp = InstalledApp(app: app,
bundleIdentifier: app.identifier,
expirationDate: Date(),
context: self.context)
installedApp = InstalledApp(app: app, bundleIdentifier: app.identifier, context: self.context)
}
installedApp.version = app.version
self.finish(.success(installedApp))
}
}
catch
{
self.finish(.failure(error))
}
}
if self.useCachedAppIfAvailable && FileManager.default.fileExists(atPath: self.ipaURL.path)
if self.sourceURL.isFileURL
{
finish(error: nil)
return
finishOperation(.success(self.sourceURL))
self.progress.completedUnitCount += 1
}
let downloadTask = self.session.downloadTask(with: self.downloadURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
try FileManager.default.copyItem(at: fileURL, to: self.ipaURL, shouldReplace: true)
finish(error: nil)
}
catch let error
{
finish(error: error)
else
{
let downloadTask = self.session.downloadTask(with: self.sourceURL) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
finishOperation(.success(fileURL))
}
catch
{
finishOperation(.failure(error))
}
}
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
}
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
}
}

View File

@@ -2,7 +2,7 @@
// InstallAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Created by Riley Testut on 6/19/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
@@ -10,277 +10,83 @@ import Foundation
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum InstallationError: LocalizedError
{
case serverNotFound
case connectionFailed
case connectionDropped
case invalidApp
var errorDescription: String? {
switch self
{
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
}
}
}
import Roxas
@objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<Void>
{
var fileURL: URL?
let context: AppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation")
private var connection: NWConnection?
override init()
init(context: AppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 4
self.progress.totalUnitCount = 100
}
override func main()
{
super.main()
guard let fileURL = self.fileURL else { return self.finish(.failure(OperationError.appNotFound)) }
if let error = self.context.error
{
self.finish(.failure(error))
return
}
// Connect to server.
self.connect { (result) in
guard
let installedApp = self.context.installedApp,
let connection = self.context.connection,
let server = self.context.group.server
else { return self.finish(.failure(OperationError.invalidParameters)) }
installedApp.managedObjectContext?.perform {
print("Installing app:", installedApp.app.identifier)
self.context.group.beginInstallationHandler?(installedApp)
}
let request = BeginInstallationRequest()
server.send(request, via: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
self.connection = connection
case .success:
// Send app to server.
self.sendApp(at: fileURL, via: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
// Receive response from server.
let progress = self.receiveResponse(from: connection) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success: self.finish(.success(()))
}
}
self.progress.addChild(progress, withPendingUnitCount: 3)
}
self.receive(from: connection, server: server) { (result) in
self.finish(result)
}
}
}
}
override func finish(_ result: Result<Void, Error>)
func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
super.finish(result)
if let connection = self.connection
{
connection.cancel()
}
}
}
private extension InstallAppOperation
{
func connect(completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(InstallationError.serverNotFound)) }
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
server.receive(ServerResponse.self, from: connection) { (result) in
do
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
completionHandler(.failure(InstallationError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
let response = try result.get()
print(response)
case .ready:
completionHandler(.success(connection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallationError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
let request = ServerRequest(udid: udid, contentSize: appData.count)
let requestData: Data
do {
requestData = try JSONEncoder().encode(request)
}
catch {
print("Invalid request.", error)
throw ALTServerError(.invalidRequest)
}
let requestSize = Int32(requestData.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
func process(_ error: Error?) -> Bool
{
if error != nil
if let error = response.error
{
completionHandler(.failure(InstallationError.connectionDropped))
return false
self.finish(.failure(error))
}
else if response.progress == 1.0
{
self.finish(.success(()))
}
else
{
return true
self.progress.completedUnitCount = Int64(response.progress * 100)
self.receive(from: connection, server: server, completionHandler: completionHandler)
}
}
// Send request data size.
print("Sending request data size \(requestSize)")
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send request.
print("Sending request \(request)")
connection.send(content: requestData, completion: .contentProcessed { (error) in
guard process(error) else { return }
// Send app data.
print("Sending app data (Size: \(appData.count))")
connection.send(content: appData, completion: .contentProcessed { (error) in
print("Sent app data!")
guard process(error) else { return }
completionHandler(.success(()))
})
})
})
}
catch
{
completionHandler(.failure(error))
}
}
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, Error>) -> Void) -> Progress
{
func receive(from connection: NWConnection, progress: Progress, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
print(response)
if let error = response.error
{
completionHandler(.failure(error))
}
else if response.progress == 1.0
{
completionHandler(.success(()))
}
else
{
progress.completedUnitCount = Int64(response.progress * 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
let progress = Progress.discreteProgress(totalUnitCount: 100)
receive(from: connection, progress: progress, completionHandler: completionHandler)
return progress
}
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
completionHandler(.failure(ALTServerError(error)))
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -23,9 +23,9 @@ class ResultOperation<ResultType>: Operation
{
guard !self.isFinished else { return }
super.finish()
self.resultHandler?(result)
super.finish()
}
}
@@ -73,6 +73,8 @@ class Operation: RSTOperation, ProgressReporting
override func finish()
{
guard !self.isFinished else { return }
super.finish()
if let backgroundTaskID = self.backgroundTaskID

View File

@@ -19,6 +19,9 @@ enum OperationError: LocalizedError
case unknownUDID
case invalidApp
case invalidParameters
var errorDescription: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
@@ -27,6 +30,8 @@ enum OperationError: LocalizedError
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
}
}
}

View File

@@ -0,0 +1,63 @@
//
// OperationGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
class OperationGroup
{
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var server: Server?
var signer: ALTSigner?
var error: Error?
var results = [String: Result<InstalledApp, Error>]()
private let operationQueue = OperationQueue()
private let installOperationQueue = OperationQueue()
init()
{
// Enforce only one installation at a time.
self.installOperationQueue.maxConcurrentOperationCount = 1
}
func cancel()
{
self.operationQueue.cancelAllOperations()
self.installOperationQueue.cancelAllOperations()
}
func addOperations(_ operations: [Operation])
{
for operation in operations
{
if let installOperation = operation as? InstallAppOperation
{
if let previousOperation = self.installOperationQueue.operations.last
{
// Ensures they execute in the order they're added, since isReady is still false at this point.
installOperation.addDependency(previousOperation)
}
self.installOperationQueue.addOperation(installOperation)
}
else
{
self.operationQueue.addOperation(operation)
}
}
}
}

View File

@@ -14,15 +14,13 @@ import AltSign
@objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<URL>
{
let installedApp: InstalledApp
private var context: NSManagedObjectContext?
let context: AppOperationContext
var signer: ALTSigner?
private let temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
init(installedApp: InstalledApp)
init(context: AppOperationContext)
{
self.installedApp = installedApp
self.context = installedApp.managedObjectContext
self.context = context
super.init()
@@ -33,17 +31,38 @@ class ResignAppOperation: ResultOperation<URL>
{
super.main()
guard let context = self.context else { return self.finish(.failure(OperationError.appNotFound)) }
guard let signer = self.signer else { return self.finish(.failure(OperationError.notAuthenticated)) }
do
{
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
}
catch
{
self.finish(.failure(error))
return
}
context.perform {
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let installedApp = self.context.installedApp,
let appContext = installedApp.managedObjectContext,
let signer = self.context.group.signer
else { return self.finish(.failure(OperationError.invalidParameters)) }
appContext.perform {
let appIdentifier = installedApp.app.identifier
// Register Device
self.registerCurrentDevice(for: signer.team) { (result) in
guard let _ = self.process(result) else { return }
// Register App
context.perform {
self.register(self.installedApp.app, team: signer.team) { (result) in
appContext.perform {
self.register(installedApp.app, team: signer.team) { (result) in
guard let appID = self.process(result) else { return }
// Fetch Provisioning Profile
@@ -51,27 +70,29 @@ class ResignAppOperation: ResultOperation<URL>
guard let profile = self.process(result) else { return }
// Prepare app bundle
context.perform {
appContext.perform {
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: self.installedApp) { (result) in
let prepareAppBundleProgress = self.prepareAppBundle(for: installedApp) { (result) in
guard let appBundleURL = self.process(result) else { return }
print("Resigning App:", appIdentifier)
// Resign app bundle
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profile: profile) { (result) in
guard let resignedURL = self.process(result) else { return }
// Finish
context.perform {
appContext.perform {
do
{
try FileManager.default.copyItem(at: resignedURL, to: self.installedApp.refreshedIPAURL, shouldReplace: true)
installedApp.expirationDate = profile.expirationDate
installedApp.refreshedDate = Date()
let refreshedDirectory = resignedURL.deletingLastPathComponent()
try? FileManager.default.removeItem(at: refreshedDirectory)
try FileManager.default.copyItem(at: resignedURL, to: installedApp.refreshedIPAURL, shouldReplace: true)
self.finish(.success(self.installedApp.refreshedIPAURL))
self.finish(.success(installedApp.refreshedIPAURL))
}
catch
{
@@ -107,6 +128,17 @@ class ResignAppOperation: ResultOperation<URL>
return value
}
}
override func finish(_ result: Result<URL, Error>)
{
super.finish(result)
if FileManager.default.fileExists(atPath: self.temporaryDirectory.path, isDirectory: nil)
{
do { try FileManager.default.removeItem(at: self.temporaryDirectory) }
catch { print("Failed to remove app bundle.", error) }
}
}
}
private extension ResignAppOperation
@@ -179,25 +211,21 @@ private extension ResignAppOperation
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
let ipaURL = installedApp.ipaURL
let bundleIdentifier = installedApp.bundleIdentifier
let openURL = installedApp.openAppURL
let appIdentifier = installedApp.app.identifier
let fileURL = installedApp.fileURL
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: refreshedAppDirectory.path)
{
try FileManager.default.removeItem(at: refreshedAppDirectory)
}
try FileManager.default.createDirectory(at: refreshedAppDirectory, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = self.temporaryDirectory.appendingPathComponent("App.app")
try FileManager.default.copyItem(at: fileURL, to: appBundleURL)
// Become current so we can observe progress from unzipAppBundle().
progress.becomeCurrent(withPendingUnitCount: 1)
let appBundleURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: refreshedAppDirectory)
guard let bundle = Bundle(url: appBundleURL) else { throw ALTError(.missingAppBundle) }
guard var infoDictionary = NSDictionary(contentsOf: bundle.infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }

View File

@@ -0,0 +1,128 @@
//
// SendAppOperation.swift
// AltStore
//
// Created by Riley Testut on 6/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<NWConnection>
{
let context: AppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
private var connection: NWConnection?
init(context: AppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 1
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let fileURL = self.context.resignedFileURL, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) }
// Connect to server.
self.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
self.connection = connection
// Send app to server.
self.sendApp(at: fileURL, via: connection, server: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
self.progress.completedUnitCount += 1
self.finish(.success(connection))
}
}
}
}
}
}
private extension SendAppOperation
{
func connect(to server: Server, completionHandler: @escaping (Result<NWConnection, Error>) -> Void)
{
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
completionHandler(.failure(ConnectionError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
case .ready:
completionHandler(.success(connection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
func sendApp(at fileURL: URL, via connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let appData = try? Data(contentsOf: fileURL) else { throw OperationError.invalidApp }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
let request = PrepareAppRequest(udid: udid, contentSize: appData.count)
print("Sending request \(request)")
server.send(request, via: connection) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success:
print("Sending app data (\(appData.count) bytes)")
server.send(appData, via: connection, prependSize: false) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success: completionHandler(.success(()))
}
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}

View File

@@ -0,0 +1,170 @@
//
// Server.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Network
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidResponse)
case is EncodingError: self = ALTServerError(.invalidRequest)
default:
assertionFailure("Caught unknown error type")
self = ALTServerError(.unknown)
}
}
}
enum ConnectionError: LocalizedError
{
case serverNotFound
case connectionFailed
case connectionDropped
var errorDescription: String? {
switch self
{
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
}
}
}
struct Server: Equatable
{
var service: NetService
func send<T: Encodable>(_ payload: T, via connection: NWConnection, prependSize: Bool = true, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let data: Data
if let payload = payload as? Data
{
data = payload
}
else
{
data = try JSONEncoder().encode(payload)
}
func process(_ error: Error?) -> Bool
{
if error != nil
{
completionHandler(.failure(ConnectionError.connectionDropped))
return false
}
else
{
return true
}
}
if prependSize
{
let requestSize = Int32(data.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
guard process(error) else { return }
connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
})
}
else
{
connection.send(content: data, completion: .contentProcessed { (error) in
guard process(error) else { return }
completionHandler(.success(()))
})
}
}
catch
{
print("Invalid request.", error)
completionHandler(.failure(ALTServerError(.invalidRequest)))
}
}
func receive<T: Decodable>(_ type: T.Type, from connection: NWConnection, completionHandler: @escaping (Result<T, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
do
{
let data = try self.process(data: data, error: error, from: connection)
let response = try JSONDecoder().decode(T.self, from: data)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
}
private extension Server
{
func process(data: Data?, error: NWError?, from connection: NWConnection) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -11,11 +11,6 @@ import Network
import AltKit
struct Server: Equatable
{
var service: NetService
}
class ServerManager: NSObject
{
static let shared = ServerManager()

View File

@@ -123,8 +123,7 @@ private extension UpdatesViewController
let progress = AppManager.shared.install(installedApp.app, presentingViewController: self) { (result) in
do
{
let app = try result.get()
try app.managedObjectContext?.save()
_ = try result.get()
DispatchQueue.main.async {
let installedApp = DatabaseManager.shared.persistentContainer.viewContext.object(with: installedApp.objectID) as! InstalledApp