mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-14 01:03:27 +01:00
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:
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user