Prioritizes app refresh order

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

View File

@@ -13,8 +13,17 @@ public let ALTServerServiceType = "_altserver._tcp"
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself // Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {} 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 udid: String
public var contentSize: Int 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 progress: Double
public var error: ALTServerError? { public var error: ALTServerError? {

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ extension AppDelegate
{ {
private func prepareForBackgroundFetch() 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) UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in 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) 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() 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) { 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() ServerManager.shared.stopDiscovering()
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
var shouldPresentAlert = true
do do
{ {
let results = try result.get() let results = try result.get()
shouldPresentAlert = !results.isEmpty
for (_, result) in results for (_, result) in results
{ {
guard case let .failure(error) = result else { continue } guard case let .failure(error) = result else { continue }
throw error throw error
} }
content.title = "Refreshed Apps!"
content.body = "Successfully refreshed all apps."
completionHandler(.newData) content.title = NSLocalizedString("Refreshed all apps!", comment: "")
} }
catch catch
{ {
print("Failed to refresh apps in background.", error) 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 content.body = error.localizedDescription
completionHandler(.failed) shouldPresentAlert = true
} }
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) if shouldPresentAlert
{
let request = UNNotificationRequest(identifier: "RefreshedApps", content: content, trigger: trigger) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error { let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
print(error) UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error)
}
} }
} }
switch result
{
case .failure(ConnectionError.serverNotFound): completionHandler(.newData)
case .failure: completionHandler(.failed)
case .success: completionHandler(.newData)
}
}
let group = AppManager.shared.refresh(installedApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
guard installedApp.app.identifier == App.altstoreAppID else { return }
// We're starting to install AltStore, which means the app is about to quit.
// So, we say we were successful even though we technically don't know 100% yet.
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
if let error = group.error
{
finish(.failure(error))
}
else
{
var results = group.results
results[installedApp.app.identifier] = .success(installedApp)
finish(.success(results))
}
}
group.completionHandler = { (result) in
finish(result)
} }
} }
} }

View File

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

View File

@@ -24,10 +24,11 @@ class AppManager
static let shared = AppManager() static let shared = AppManager()
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue")
private init() 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 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 do
{ {
guard let (_, result) = try result.get().first else { throw OperationError.unknown } 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 func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup
{
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
{ {
let apps = installedApps.compactMap { $0.app } let apps = installedApps.compactMap { $0.app }
let progress = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, completionHandler: completionHandler) let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
return progress return group
} }
} }
private extension AppManager 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 { guard let server = ServerManager.shared.discoveredServers.first else {
completionHandler(.success([:])) DispatchQueue.main.async {
return progress group.completionHandler?(.failure(ConnectionError.serverNotFound))
}
return group
} }
// Authenticate group.server = server
var operations = [Operation]()
/* Authenticate */
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in authenticationOperation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): completionHandler(.failure(error)) case .failure(let error): group.error = error
case .success(let signer): case .success(let signer): group.signer = 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))
}
}
}
} }
} }
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 do
{
let resignAppOperation = ResignAppOperation(installedApp: installedApp) let value = try result.get()
let installAppOperation = InstallAppOperation() return value
// 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
}
} }
catch
// Install {
installAppOperation.addDependency(resignAppOperation) context.error = error
installAppOperation.resultHandler = { (result) in return nil
switch result }
}
func finishAppOperation(_ context: AppOperationContext)
{
self.processingQueue.sync {
if let error = context.error
{ {
case .failure(let error): completionHandler(.failure(error)) context.group.results[context.appIdentifier] = .failure(error)
case .success: }
context?.perform { else if let installedApp = context.installedApp
completionHandler(.success(installedApp)) {
context.group.results[context.appIdentifier] = .success(installedApp)
// Save after each installation.
installedApp.managedObjectContext?.perform {
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
} }
} }
print("Finished operation!", context.appIdentifier)
if context.group.results.count == context.group.progress.totalUnitCount
{
context.group.completionHandler?(.success(context.group.results))
}
} }
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
return (resignAppOperation.progress, installAppOperation.progress)
} }
} }

View File

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

View File

@@ -8,6 +8,7 @@
import CoreData import CoreData
import AltSign
import Roxas import Roxas
public class DatabaseManager public class DatabaseManager
@@ -120,14 +121,23 @@ private extension DatabaseManager
altStoreApp.version = version altStoreApp.version = version
} }
if let installedApp = altStoreApp.installedApp let installedApp: InstalledApp
if let app = altStoreApp.installedApp
{ {
installedApp.version = version installedApp = app
} }
else else
{ {
let installedApp = InstalledApp(app: altStoreApp, bundleIdentifier: altStoreApp.identifier, expirationDate: Date(), context: context) installedApp = InstalledApp(app: altStoreApp, bundleIdentifier: altStoreApp.identifier, context: context)
installedApp.version = version }
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 do

View File

@@ -16,6 +16,7 @@ class InstalledApp: NSManagedObject, Fetchable
@NSManaged var bundleIdentifier: String @NSManaged var bundleIdentifier: String
@NSManaged var version: String @NSManaged var version: String
@NSManaged var refreshedDate: Date
@NSManaged var expirationDate: Date @NSManaged var expirationDate: Date
/* Relationships */ /* Relationships */
@@ -26,7 +27,7 @@ class InstalledApp: NSManagedObject, Fetchable
super.init(entity: entity, insertInto: context) 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) super.init(entity: InstalledApp.entity(), insertInto: context)
@@ -35,7 +36,9 @@ class InstalledApp: NSManagedObject, Fetchable
self.version = app.version self.version = app.version
self.bundleIdentifier = bundleIdentifier 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") 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 extension InstalledApp
@@ -67,10 +116,10 @@ extension InstalledApp
return appsDirectoryURL 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") let appURL = self.directoryURL(for: app).appendingPathComponent("App.app")
return ipaURL return appURL
} }
class func refreshedIPAURL(for app: App) -> URL class func refreshedIPAURL(for app: App) -> URL
@@ -79,7 +128,6 @@ extension InstalledApp
return ipaURL return ipaURL
} }
class func directoryURL(for app: App) -> URL class func directoryURL(for app: App) -> URL
{ {
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier) let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
@@ -94,8 +142,8 @@ extension InstalledApp
return InstalledApp.directoryURL(for: self.app) return InstalledApp.directoryURL(for: self.app)
} }
var ipaURL: URL { var fileURL: URL {
return InstalledApp.ipaURL(for: self.app) return InstalledApp.fileURL(for: self.app)
} }
var refreshedIPAURL: URL { var refreshedIPAURL: URL {

View File

@@ -24,6 +24,8 @@ class MyAppsViewController: UITableViewController
return dateFormatter return dateFormatter
}() }()
private var refreshGroup: OperationGroup?
@IBOutlet private var progressView: UIProgressView! @IBOutlet private var progressView: UIProgressView!
override func viewDidLoad() override func viewDidLoad()
@@ -119,9 +121,25 @@ private extension MyAppsViewController
{ {
sender.isIndicatingActivity = true 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 { DispatchQueue.main.async {
switch result switch result
{ {
@@ -164,46 +182,16 @@ private extension MyAppsViewController
self.progressView.observedProgress = nil self.progressView.observedProgress = nil
self.progressView.progress = 0.0 self.progressView.progress = 0.0
sender.isIndicatingActivity = false
self.update() 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 { self.refreshGroup = nil
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil) completionHandler(result)
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.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 refreshAction = UITableViewRowAction(style: .normal, title: "Refresh") { (action, indexPath) in
let installedApp = self.dataSource.item(at: indexPath) let installedApp = self.dataSource.item(at: indexPath)
self.refresh([installedApp]) { (result) in
let toastView = RSTToastView(text: "Refreshing...", detailText: nil) print("Refreshed", installedApp.app.identifier)
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.progressView.observedProgress = progress
} }
return [deleteAction, refreshAction] return [deleteAction, refreshAction]

View File

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

View File

@@ -19,16 +19,18 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
var useCachedAppIfAvailable = false var useCachedAppIfAvailable = false
lazy var context = DatabaseManager.shared.persistentContainer.newBackgroundContext() lazy var context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
private let downloadURL: URL private let appIdentifier: String
private let ipaURL: URL private let sourceURL: URL
private let destinationURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
init(app: App) init(app: App)
{ {
self.app = app self.app = app
self.downloadURL = app.downloadURL self.appIdentifier = app.identifier
self.ipaURL = InstalledApp.ipaURL(for: app) self.sourceURL = app.downloadURL
self.destinationURL = InstalledApp.fileURL(for: app)
super.init() super.init()
@@ -39,14 +41,36 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
{ {
super.main() super.main()
func finish(error: Error?) print("Downloading App:", self.appIdentifier)
func finishOperation(_ result: Result<URL, Error>)
{ {
if let error = error do
{
self.finish(.failure(error))
}
else
{ {
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 { self.context.perform {
let app = self.context.object(with: self.app.objectID) as! App let app = self.context.object(with: self.app.objectID) as! App
@@ -59,40 +83,41 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
} }
else else
{ {
installedApp = InstalledApp(app: app, installedApp = InstalledApp(app: app, bundleIdentifier: app.identifier, context: self.context)
bundleIdentifier: app.identifier,
expirationDate: Date(),
context: self.context)
} }
installedApp.version = app.version installedApp.version = app.version
self.finish(.success(installedApp)) 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) finishOperation(.success(self.sourceURL))
return
self.progress.completedUnitCount += 1
} }
else
let downloadTask = self.session.downloadTask(with: self.downloadURL) { (fileURL, response, error) in {
do let downloadTask = self.session.downloadTask(with: self.sourceURL) { (fileURL, response, error) in
{ do
let (fileURL, _) = try Result((fileURL, response), error).get() {
let (fileURL, _) = try Result((fileURL, response), error).get()
try FileManager.default.copyItem(at: fileURL, to: self.ipaURL, shouldReplace: true) finishOperation(.success(fileURL))
}
finish(error: nil) catch
} {
catch let error finishOperation(.failure(error))
{ }
finish(error: error)
} }
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
} }
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
downloadTask.resume()
} }
} }

View File

@@ -2,7 +2,7 @@
// InstallAppOperation.swift // InstallAppOperation.swift
// AltStore // AltStore
// //
// Created by Riley Testut on 6/7/19. // Created by Riley Testut on 6/19/19.
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
@@ -10,277 +10,83 @@ import Foundation
import Network import Network
import AltKit import AltKit
import Roxas
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: "")
}
}
}
@objc(InstallAppOperation) @objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<Void> class InstallAppOperation: ResultOperation<Void>
{ {
var fileURL: URL? let context: AppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation") init(context: AppOperationContext)
private var connection: NWConnection?
override init()
{ {
self.context = context
super.init() super.init()
self.progress.totalUnitCount = 4 self.progress.totalUnitCount = 100
} }
override func main() override func main()
{ {
super.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. guard
self.connect { (result) in 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 switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let connection): case .success:
self.connection = connection
// Send app to server. self.receive(from: connection, server: server) { (result) in
self.sendApp(at: fileURL, via: connection) { (result) in self.finish(result)
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)
}
} }
} }
} }
} }
override func finish(_ result: Result<Void, Error>) func receive(from connection: NWConnection, server: Server, completionHandler: @escaping (Result<Void, Error>) -> Void)
{ {
super.finish(result) server.receive(ServerResponse.self, from: connection) { (result) in
do
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
{ {
case .failed(let error): let response = try result.get()
print("Failed to connect to service \(server.service.name).", error) print(response)
completionHandler(.failure(InstallationError.connectionFailed))
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
case .ready: if let error = response.error
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
{ {
completionHandler(.failure(InstallationError.connectionDropped)) self.finish(.failure(error))
return false }
else if response.progress == 1.0
{
self.finish(.success(()))
} }
else 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 catch
{ {
throw error completionHandler(.failure(ALTServerError(error)))
} }
} }
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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