mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +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:
@@ -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? {
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
35
AltStore/Operations/AppOperationContext.swift
Normal file
35
AltStore/Operations/AppOperationContext.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
AltStore/Operations/OperationGroup.swift
Normal file
63
AltStore/Operations/OperationGroup.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
|
||||
128
AltStore/Operations/SendAppOperation.swift
Normal file
128
AltStore/Operations/SendAppOperation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
170
AltStore/Server/Server.swift
Normal file
170
AltStore/Server/Server.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,6 @@ import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
}
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
static let shared = ServerManager()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user