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

@@ -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.")
}
}
}