mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Displays progress when downloading/refreshing apps
Refactors download/refresh steps into separate Operation subclasses
This commit is contained in:
@@ -27,7 +27,8 @@ public struct ServerRequest: Codable
|
||||
|
||||
public struct ServerResponse: Codable
|
||||
{
|
||||
public var success: Bool
|
||||
public var progress: Double
|
||||
|
||||
public var error: ALTServerError? {
|
||||
get {
|
||||
guard let code = self.errorCode else { return nil }
|
||||
@@ -37,12 +38,11 @@ public struct ServerResponse: Codable
|
||||
self.errorCode = newValue?.code
|
||||
}
|
||||
}
|
||||
|
||||
private var errorCode: ALTServerError.Code?
|
||||
|
||||
public init(success: Bool, error: ALTServerError?)
|
||||
public init(progress: Double, error: ALTServerError?)
|
||||
{
|
||||
self.success = success
|
||||
self.progress = progress
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,8 +184,7 @@ private extension ConnectionManager
|
||||
print("Processed request from \(connection.endpoint).")
|
||||
}
|
||||
|
||||
let success = (error == nil)
|
||||
let response = ServerResponse(success: success, error: error)
|
||||
let response = ServerResponse(progress: 1.0, error: error)
|
||||
|
||||
self.send(response, to: connection) { (result) in
|
||||
print("Sent response to \(connection.endpoint) with result:", result)
|
||||
@@ -209,12 +208,15 @@ private extension ConnectionManager
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success(let request, let fileURL):
|
||||
print("Installing to device..")
|
||||
print("Installing to device \(request.udid)...")
|
||||
|
||||
ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: request.udid) { (success, error) in
|
||||
print("Installed app with result:", result)
|
||||
let error = error.map { $0 as? ALTServerError ?? ALTServerError(.unknown) }
|
||||
finish(error: 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): finish(error: error)
|
||||
case .success: finish(error: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,6 +305,46 @@ private extension ConnectionManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
||||
var isSending = false
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
|
||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid) { (success, error) in
|
||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
||||
|
||||
if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
|
||||
observation?.invalidate()
|
||||
observation = nil
|
||||
}
|
||||
|
||||
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
|
||||
serialQueue.async {
|
||||
guard !isSending else { return }
|
||||
isSending = true
|
||||
|
||||
print("Progress:", progress.fractionCompleted)
|
||||
let response = ServerResponse(progress: progress.fractionCompleted, error: nil)
|
||||
|
||||
self.send(response, to: connection) { (result) in
|
||||
serialQueue.async {
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func send(_ response: ServerResponse, to connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(void)> *installationCompletionHandlers;
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSValue *> *installationClients;
|
||||
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
|
||||
|
||||
@end
|
||||
|
||||
@@ -49,6 +50,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
_installationCompletionHandlers = [NSMutableDictionary dictionary];
|
||||
_installationProgress = [NSMutableDictionary dictionary];
|
||||
_installationClients = [NSMutableDictionary dictionary];
|
||||
|
||||
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -56,222 +59,239 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:100];
|
||||
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:4];
|
||||
|
||||
NSUUID *UUID = [NSUUID UUID];
|
||||
__block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
|
||||
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
|
||||
uuidString[UUID.UUIDString.length] = '\0';
|
||||
|
||||
idevice_t device = NULL;
|
||||
lockdownd_client_t client = NULL;
|
||||
instproxy_client_t ipc = NULL;
|
||||
np_client_t np = NULL;
|
||||
afc_client_t afc = NULL;
|
||||
lockdownd_service_descriptor_t service = NULL;
|
||||
|
||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
||||
np_client_free(np);
|
||||
instproxy_client_free(ipc);
|
||||
afc_client_free(afc);
|
||||
lockdownd_client_free(client);
|
||||
idevice_free(device);
|
||||
lockdownd_service_descriptor_free(service);
|
||||
dispatch_async(self.installationQueue, ^{
|
||||
NSUUID *UUID = [NSUUID UUID];
|
||||
__block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
|
||||
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
|
||||
uuidString[UUID.UUIDString.length] = '\0';
|
||||
|
||||
free(uuidString);
|
||||
uuidString = NULL;
|
||||
idevice_t device = NULL;
|
||||
lockdownd_client_t client = NULL;
|
||||
instproxy_client_t ipc = NULL;
|
||||
np_client_t np = NULL;
|
||||
afc_client_t afc = NULL;
|
||||
lockdownd_service_descriptor_t service = NULL;
|
||||
|
||||
if (error != nil)
|
||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
||||
np_client_free(np);
|
||||
instproxy_client_free(ipc);
|
||||
afc_client_free(afc);
|
||||
lockdownd_client_free(client);
|
||||
idevice_free(device);
|
||||
lockdownd_service_descriptor_free(service);
|
||||
|
||||
free(uuidString);
|
||||
uuidString = NULL;
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
completionHandler(NO, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
};
|
||||
|
||||
NSURL *appBundleURL = nil;
|
||||
NSURL *temporaryDirectoryURL = nil;
|
||||
|
||||
if ([fileURL.pathExtension.lowercaseString isEqualToString:@"app"])
|
||||
{
|
||||
completionHandler(NO, error);
|
||||
appBundleURL = fileURL;
|
||||
temporaryDirectoryURL = nil;
|
||||
}
|
||||
else if ([fileURL.pathExtension.lowercaseString isEqualToString:@"ipa"])
|
||||
{
|
||||
NSLog(@"Unzipping .ipa...");
|
||||
|
||||
temporaryDirectoryURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[[NSUUID UUID] UUIDString] isDirectory:YES];
|
||||
|
||||
NSError *error = nil;
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error])
|
||||
{
|
||||
return finish(error);
|
||||
}
|
||||
|
||||
appBundleURL = [[NSFileManager defaultManager] unzipAppBundleAtURL:fileURL toDirectory:temporaryDirectoryURL error:&error];
|
||||
if (appBundleURL == nil)
|
||||
{
|
||||
return finish(error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
return finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]);
|
||||
}
|
||||
};
|
||||
|
||||
NSURL *appBundleURL = nil;
|
||||
NSURL *temporaryDirectoryURL = nil;
|
||||
|
||||
if ([fileURL.pathExtension.lowercaseString isEqualToString:@"app"])
|
||||
{
|
||||
appBundleURL = fileURL;
|
||||
temporaryDirectoryURL = nil;
|
||||
}
|
||||
else if ([fileURL.pathExtension.lowercaseString isEqualToString:@"ipa"])
|
||||
{
|
||||
NSLog(@"Unzipping .ipa...");
|
||||
|
||||
temporaryDirectoryURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:[[NSUUID UUID] UUIDString] isDirectory:YES];
|
||||
|
||||
NSError *error = nil;
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error])
|
||||
/* Find Device */
|
||||
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
finish(error);
|
||||
return progress;
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
}
|
||||
|
||||
appBundleURL = [[NSFileManager defaultManager] unzipAppBundleAtURL:fileURL toDirectory:temporaryDirectoryURL error:&error];
|
||||
if (appBundleURL == nil)
|
||||
/* Connect to Device */
|
||||
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
|
||||
{
|
||||
finish(error);
|
||||
return progress;
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
/* Find Device */
|
||||
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
/* Connect to Device */
|
||||
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
/* Connect to Notification Proxy */
|
||||
if ((lockdownd_start_service(client, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
if (np_client_new(device, service, &np) != NP_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
np_set_notify_callback(np, ALTDeviceManagerDidFinishAppInstallation, uuidString);
|
||||
|
||||
const char *notifications[2] = { NP_APP_INSTALLED, NULL };
|
||||
np_observe_notifications(np, notifications);
|
||||
|
||||
if (service)
|
||||
{
|
||||
|
||||
/* Connect to Notification Proxy */
|
||||
if ((lockdownd_start_service(client, "com.apple.mobile.notification_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
if (np_client_new(device, service, &np) != NP_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
np_set_notify_callback(np, ALTDeviceManagerDidFinishAppInstallation, uuidString);
|
||||
|
||||
const char *notifications[2] = { NP_APP_INSTALLED, NULL };
|
||||
np_observe_notifications(np, notifications);
|
||||
|
||||
if (service)
|
||||
{
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
}
|
||||
|
||||
/* Connect to Installation Proxy */
|
||||
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
if (service)
|
||||
{
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
}
|
||||
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
}
|
||||
|
||||
/* Connect to Installation Proxy */
|
||||
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
if (service)
|
||||
{
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
}
|
||||
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
|
||||
/* Connect to AFC service */
|
||||
if ((lockdownd_start_service(client, "com.apple.afc", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
lockdownd_client_free(client);
|
||||
client = NULL;
|
||||
|
||||
if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
|
||||
NSURL *stagingURL = [NSURL fileURLWithPath:@"PublicStaging" isDirectory:YES];
|
||||
|
||||
/* Prepare for installation */
|
||||
char **files = NULL;
|
||||
if (afc_get_file_info(afc, stagingURL.relativePath.fileSystemRepresentation, &files) != AFC_E_SUCCESS)
|
||||
{
|
||||
if (afc_make_directory(afc, stagingURL.relativePath.fileSystemRepresentation) != AFC_E_SUCCESS)
|
||||
{
|
||||
finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceWriteFailed userInfo:nil]);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
if (files)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
while (files[i])
|
||||
/* Connect to AFC service */
|
||||
if ((lockdownd_start_service(client, "com.apple.afc", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
free(files[i]);
|
||||
i++;
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
free(files);
|
||||
}
|
||||
|
||||
NSLog(@"Writing to device...");
|
||||
|
||||
plist_t options = instproxy_client_options_new();
|
||||
instproxy_client_options_add(options, "PackageType", "Developer", NULL);
|
||||
|
||||
NSURL *destinationURL = [stagingURL URLByAppendingPathComponent:appBundleURL.lastPathComponent];
|
||||
|
||||
NSError *writeError = nil;
|
||||
if (![self writeDirectory:appBundleURL toDestinationURL:destinationURL client:afc error:&writeError])
|
||||
{
|
||||
finish(writeError);
|
||||
return progress;
|
||||
}
|
||||
|
||||
NSLog(@"Finished writing to device.");
|
||||
|
||||
NSValue *value = [NSValue valueWithPointer:(const void *)np];
|
||||
|
||||
self.installationClients[UUID] = value;
|
||||
self.installationProgress[UUID] = progress;
|
||||
self.installationCompletionHandlers[UUID] = ^{
|
||||
finish(nil);
|
||||
lockdownd_client_free(client);
|
||||
client = NULL;
|
||||
|
||||
if (temporaryDirectoryURL != nil)
|
||||
if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
|
||||
{
|
||||
NSError *error = nil;
|
||||
if (![[NSFileManager defaultManager] removeItemAtURL:temporaryDirectoryURL error:&error])
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
NSURL *stagingURL = [NSURL fileURLWithPath:@"PublicStaging" isDirectory:YES];
|
||||
|
||||
/* Prepare for installation */
|
||||
char **files = NULL;
|
||||
if (afc_get_file_info(afc, stagingURL.relativePath.fileSystemRepresentation, &files) != AFC_E_SUCCESS)
|
||||
{
|
||||
if (afc_make_directory(afc, stagingURL.relativePath.fileSystemRepresentation) != AFC_E_SUCCESS)
|
||||
{
|
||||
NSLog(@"Error removing temporary directory. %@", error);
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceWriteFailed userInfo:nil]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NSLog(@"Installing to device %@...", udid);
|
||||
|
||||
instproxy_install(ipc, destinationURL.relativePath.fileSystemRepresentation, options, ALTDeviceManagerUpdateStatus, uuidString);
|
||||
instproxy_client_options_free(options);
|
||||
|
||||
if (files)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
while (files[i])
|
||||
{
|
||||
free(files[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
free(files);
|
||||
}
|
||||
|
||||
NSLog(@"Writing to device...");
|
||||
|
||||
plist_t options = instproxy_client_options_new();
|
||||
instproxy_client_options_add(options, "PackageType", "Developer", NULL);
|
||||
|
||||
NSURL *destinationURL = [stagingURL URLByAppendingPathComponent:appBundleURL.lastPathComponent];
|
||||
|
||||
// Writing files to device should be worth 3/4 of total work.
|
||||
[progress becomeCurrentWithPendingUnitCount:3];
|
||||
|
||||
NSError *writeError = nil;
|
||||
if (![self writeDirectory:appBundleURL toDestinationURL:destinationURL client:afc progress:nil error:&writeError])
|
||||
{
|
||||
return finish(writeError);
|
||||
}
|
||||
|
||||
NSLog(@"Finished writing to device.");
|
||||
|
||||
NSProgress *installationProgress = [NSProgress progressWithTotalUnitCount:100 parent:progress pendingUnitCount:1];
|
||||
|
||||
NSValue *value = [NSValue valueWithPointer:(const void *)np];
|
||||
|
||||
self.installationClients[UUID] = value;
|
||||
self.installationProgress[UUID] = installationProgress;
|
||||
self.installationCompletionHandlers[UUID] = ^{
|
||||
finish(nil);
|
||||
|
||||
if (temporaryDirectoryURL != nil)
|
||||
{
|
||||
NSError *error = nil;
|
||||
if (![[NSFileManager defaultManager] removeItemAtURL:temporaryDirectoryURL error:&error])
|
||||
{
|
||||
NSLog(@"Error removing temporary directory. %@", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NSLog(@"Installing to device %@...", udid);
|
||||
|
||||
instproxy_install(ipc, destinationURL.relativePath.fileSystemRepresentation, options, ALTDeviceManagerUpdateStatus, uuidString);
|
||||
instproxy_client_options_free(options);
|
||||
});
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
- (BOOL)writeDirectory:(NSURL *)directoryURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error
|
||||
- (BOOL)writeDirectory:(NSURL *)directoryURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc progress:(NSProgress *)progress error:(NSError **)error
|
||||
{
|
||||
afc_make_directory(afc, destinationURL.relativePath.fileSystemRepresentation);
|
||||
|
||||
if (progress == nil)
|
||||
{
|
||||
NSDirectoryEnumerator *countEnumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL
|
||||
includingPropertiesForKeys:@[]
|
||||
options:0
|
||||
errorHandler:^BOOL(NSURL * _Nonnull url, NSError * _Nonnull error) {
|
||||
if (error) {
|
||||
NSLog(@"[Error] %@ (%@)", error, url);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}];
|
||||
|
||||
NSInteger totalCount = 0;
|
||||
for (NSURL *__unused fileURL in countEnumerator)
|
||||
{
|
||||
totalCount++;
|
||||
}
|
||||
|
||||
progress = [NSProgress progressWithTotalUnitCount:totalCount];
|
||||
}
|
||||
|
||||
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL
|
||||
includingPropertiesForKeys:@[NSURLIsDirectoryKey]
|
||||
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
|
||||
@@ -295,7 +315,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
if ([isDirectory boolValue])
|
||||
{
|
||||
NSURL *destinationDirectoryURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:YES];
|
||||
if (![self writeDirectory:fileURL toDestinationURL:destinationDirectoryURL client:afc error:error])
|
||||
if (![self writeDirectory:fileURL toDestinationURL:destinationDirectoryURL client:afc progress:progress error:error])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
@@ -308,6 +328,8 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
progress.completedUnitCount += 1;
|
||||
}
|
||||
|
||||
return YES;
|
||||
@@ -315,12 +337,19 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
|
||||
- (BOOL)writeFile:(NSURL *)fileURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error
|
||||
{
|
||||
NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:error];
|
||||
if (data == nil)
|
||||
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:fileURL.path];
|
||||
if (fileHandle == nil)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{NSURLErrorKey: fileURL}];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSData *data = [fileHandle readDataToEndOfFile];
|
||||
|
||||
uint64_t af = 0;
|
||||
if ((afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || af == 0)
|
||||
{
|
||||
@@ -334,11 +363,12 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||
|
||||
BOOL success = YES;
|
||||
uint32_t bytesWritten = 0;
|
||||
|
||||
|
||||
while (bytesWritten < data.length)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
if (afc_file_write(afc, af, (const char *)data.bytes, (uint32_t)data.length, &bytesWritten) != AFC_E_SUCCESS)
|
||||
|
||||
if (afc_file_write(afc, af, (const char *)data.bytes + bytesWritten, (uint32_t)data.length - bytesWritten, &count) != AFC_E_SUCCESS)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
|
||||
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; };
|
||||
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; };
|
||||
@@ -112,7 +113,6 @@
|
||||
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */; };
|
||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; };
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; };
|
||||
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD522A08A85000B7ED1 /* Server.swift */; };
|
||||
BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; };
|
||||
BFD52C0222A1A9CB000B7ED1 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE622A1A9CA000B7ED1 /* base64.c */; };
|
||||
BFD52C0322A1A9CB000B7ED1 /* hashtable.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE722A1A9CA000B7ED1 /* hashtable.c */; };
|
||||
@@ -146,6 +146,10 @@
|
||||
BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; };
|
||||
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; };
|
||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -315,6 +319,7 @@
|
||||
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
|
||||
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
|
||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -331,7 +336,6 @@
|
||||
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = "<group>"; };
|
||||
BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = "<group>"; };
|
||||
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
|
||||
BFD52BD522A08A85000B7ED1 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
|
||||
BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ptrarray.c; path = Dependencies/libplist/src/ptrarray.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD52BE622A1A9CA000B7ED1 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = base64.c; path = Dependencies/libplist/src/base64.c; sourceTree = SOURCE_ROOT; };
|
||||
BFD52BE722A1A9CA000B7ED1 /* hashtable.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = hashtable.c; path = Dependencies/libplist/src/hashtable.c; sourceTree = SOURCE_ROOT; };
|
||||
@@ -365,6 +369,10 @@
|
||||
BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; };
|
||||
BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; };
|
||||
BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -641,7 +649,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD52BD322A0800A000B7ED1 /* ServerManager.swift */,
|
||||
BFD52BD522A08A85000B7ED1 /* Server.swift */,
|
||||
);
|
||||
path = Server;
|
||||
sourceTree = "<group>";
|
||||
@@ -682,6 +689,7 @@
|
||||
BFDB69FB22A9A7A6007EA6D6 /* Account */,
|
||||
BFC51D7922972F1F00388324 /* Server */,
|
||||
BFD247982284D7FC00981D42 /* Model */,
|
||||
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
|
||||
BFD2478D2284C4C700981D42 /* Components */,
|
||||
BFDB6A0622A9B114007EA6D6 /* Protocols */,
|
||||
BFD2479D2284FBBD00981D42 /* Extensions */,
|
||||
@@ -793,6 +801,19 @@
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFDB6A0922AAEDA1007EA6D6 /* Operations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */,
|
||||
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
|
||||
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
|
||||
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFE6325822A83BA800F30809 /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -800,7 +821,6 @@
|
||||
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */,
|
||||
BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */,
|
||||
BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */,
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
@@ -1148,12 +1168,15 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */,
|
||||
BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */,
|
||||
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */,
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||
@@ -1163,14 +1186,15 @@
|
||||
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */,
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
|
||||
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */,
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
|
||||
BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */,
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
||||
BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */,
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
|
||||
|
||||
@@ -62,7 +62,7 @@ extension AppDelegate
|
||||
private func prepareForBackgroundFetch()
|
||||
{
|
||||
// Fetch every 6 hours.
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60 * 6)
|
||||
UIApplication.shared.setMinimumBackgroundFetchInterval(60 * 60)
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
||||
}
|
||||
@@ -72,8 +72,10 @@ extension AppDelegate
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
AppManager.shared.refreshAllApps(presentingViewController: nil) { (result) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
_ = AppManager.shared.refresh(installedApps, presentingViewController: nil) { (result) in
|
||||
ServerManager.shared.stopDiscovering()
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
@@ -87,9 +89,7 @@ extension AppDelegate
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
print(results)
|
||||
|
||||
|
||||
content.title = "Refreshed Apps!"
|
||||
content.body = "Successfully refreshed all apps."
|
||||
|
||||
|
||||
@@ -124,7 +124,11 @@ private extension AppDetailViewController
|
||||
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
||||
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()
|
||||
@@ -138,7 +142,7 @@ private extension AppDetailViewController
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled)
|
||||
catch OperationError.cancelled
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
@@ -150,12 +154,28 @@ private extension AppDetailViewController
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
progressView.alpha = 0.0
|
||||
}) { _ in
|
||||
progressView.removeFromSuperview()
|
||||
}
|
||||
|
||||
sender.isIndicatingActivity = false
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
progressView.observedProgress = progress
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
navigationBar.addSubview(progressView)
|
||||
|
||||
NSLayoutConstraint.activate([progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
|
||||
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,52 +14,10 @@ import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
enum AppError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case missingUDID
|
||||
case noServersFound
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
case notAuthenticated
|
||||
|
||||
case multipleCertificates
|
||||
case multipleTeams
|
||||
|
||||
case download(URLError)
|
||||
case authentication(Error)
|
||||
case fetchingSigningResources(Error)
|
||||
case prepare(Error)
|
||||
case install(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .unknown: return "An unknown error occured."
|
||||
case .missingUDID: return "The UDID for this device is unknown."
|
||||
case .noServersFound: return "An active AltServer could not be found."
|
||||
case .missingPrivateKey: return "A valid private key must be provided."
|
||||
case .missingCertificate: return "A valid certificate must be provided."
|
||||
case .notAuthenticated: return "You must be logged in with your Apple ID to install apps."
|
||||
case .multipleCertificates: return "You must select a certificate to use to install apps."
|
||||
case .multipleTeams: return "You must select a team to use to install apps."
|
||||
case .download(let error): return error.localizedDescription
|
||||
case .authentication(let error): return error.localizedDescription
|
||||
case .fetchingSigningResources(let error): return error.localizedDescription
|
||||
case .prepare(let error): return error.localizedDescription
|
||||
case .install(let error): return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
private init()
|
||||
@@ -107,7 +65,7 @@ extension AppManager
|
||||
#endif
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void)
|
||||
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<ALTSigner, Error>) -> Void)
|
||||
{
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
@@ -119,91 +77,60 @@ extension AppManager
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, AppError>) -> Void)
|
||||
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let ipaURL = InstalledApp.ipaURL(for: app)
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.InstallApp")
|
||||
|
||||
func finish(_ result: Result<InstalledApp, AppError>)
|
||||
{
|
||||
completionHandler(result)
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
}
|
||||
|
||||
// Download app
|
||||
self.downloadApp(from: app.downloadURL) { (result) in
|
||||
let result = result.flatMap { (fileURL) -> Result<Void, URLError> in
|
||||
// Copy downloaded app to proper location
|
||||
let result = Result { try FileManager.default.copyItem(at: fileURL, to: ipaURL, shouldReplace: true) }
|
||||
return result.mapError { _ in URLError(.cannotWriteToFile) }
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.download(error)))
|
||||
case .success:
|
||||
// Authenticate
|
||||
self.authenticate(presentingViewController: presentingViewController) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team, let certificate):
|
||||
|
||||
// Fetch provisioning profile
|
||||
self.prepareProvisioningProfile(for: app, team: team) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.fetchingSigningResources(error)))
|
||||
case .success(let profile):
|
||||
|
||||
// Prepare app
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: app.objectID) as! App
|
||||
|
||||
let installedApp = InstalledApp(app: app,
|
||||
bundleIdentifier: profile.appID.bundleIdentifier,
|
||||
expirationDate: profile.expirationDate,
|
||||
context: context)
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.prepare(error)))
|
||||
case .success(let resignedURL):
|
||||
|
||||
// Send app to server
|
||||
context.perform {
|
||||
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.install(error)))
|
||||
case .success:
|
||||
context.perform {
|
||||
finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let signer):
|
||||
|
||||
// Download
|
||||
app.managedObjectContext?.perform {
|
||||
let downloadAppOperation = DownloadAppOperation(app: app)
|
||||
downloadAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let installedApp):
|
||||
let context = installedApp.managedObjectContext
|
||||
|
||||
// Refresh/Install
|
||||
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
context?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.addChild(resignProgress, withPendingUnitCount: 10)
|
||||
progress.addChild(installProgress, withPendingUnitCount: 45)
|
||||
}
|
||||
}
|
||||
progress.addChild(downloadAppOperation.progress, withPendingUnitCount: 40)
|
||||
self.operationQueue.addOperation(downloadAppOperation)
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.addChild(authenticationOperation.progress, withPendingUnitCount: 5)
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
func refresh(_ app: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
self.refresh([app], presentingViewController: presentingViewController) { (result) in
|
||||
return self.refresh([app], presentingViewController: presentingViewController) { (result) in
|
||||
do
|
||||
{
|
||||
guard let (_, result) = try result.get().first else { throw AppError.unknown }
|
||||
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
|
||||
completionHandler(result)
|
||||
}
|
||||
catch
|
||||
@@ -213,303 +140,101 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAllApps(presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void)
|
||||
@discardableResult func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void) -> Progress
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
|
||||
|
||||
let installedApps = try context.fetch(fetchRequest)
|
||||
self.refresh(installedApps, presentingViewController: presentingViewController) { (result) in
|
||||
context.perform { // keep context alive
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(.prepare(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh<T: Collection>(_ installedApps: T, presentingViewController: UIViewController?, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], AppError>) -> Void) where T.Element == InstalledApp
|
||||
{
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.RefreshApps")
|
||||
let progress = Progress.discreteProgress(totalUnitCount: Int64(installedApps.count))
|
||||
|
||||
func finish(_ result: Result<[String: Result<InstalledApp, Error>], AppError>)
|
||||
{
|
||||
completionHandler(result)
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
guard let context = installedApps.first?.managedObjectContext else {
|
||||
completionHandler(.success([:]))
|
||||
return progress
|
||||
}
|
||||
|
||||
guard !ServerManager.shared.discoveredServers.isEmpty else { return finish(.failure(.noServersFound)) }
|
||||
|
||||
// Authenticate
|
||||
self.authenticate(presentingViewController: presentingViewController) { (result) in
|
||||
let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController)
|
||||
authenticationOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(.authentication(error)))
|
||||
case .success(let team, let certificate):
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let signer):
|
||||
|
||||
// Sign
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
for app in installedApps
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
// Refresh
|
||||
context.perform {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
let bundleIdentifier = app.bundleIdentifier
|
||||
for installedApp in installedApps
|
||||
{
|
||||
let bundleIdentifier = installedApp.bundleIdentifier
|
||||
print("Refreshing App:", bundleIdentifier)
|
||||
|
||||
self.refresh(app, signer: signer, context: context) { (result) in
|
||||
dispatchGroup.enter()
|
||||
|
||||
let (resignProgress, installProgress) = self.refresh(installedApp, signer: signer, presentingViewController: presentingViewController) { (result) in
|
||||
print("Refreshed App: \(bundleIdentifier).", result)
|
||||
results[bundleIdentifier] = result
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let refreshProgress = Progress(totalUnitCount: 100)
|
||||
refreshProgress.addChild(resignProgress, withPendingUnitCount: 20)
|
||||
refreshProgress.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
progress.addChild(refreshProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
finish(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, URLError>) -> Void)
|
||||
{
|
||||
let downloadTask = self.session.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
completionHandler(.success(fileURL))
|
||||
}
|
||||
catch let error as URLError
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(URLError(.unknown)))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) }
|
||||
|
||||
let device = ALTDevice(name: UIDevice.current.name, identifier: udid)
|
||||
self.register(device, team: team) { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
app.managedObjectContext?.perform {
|
||||
self.register(app, with: team) { (result) in
|
||||
do
|
||||
{
|
||||
let appID = try result.get()
|
||||
self.fetchProvisioningProfile(for: appID, team: team) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
completionHandler(.success(provisioningProfile))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let refreshedAppDirectory = installedApp.directoryURL.appendingPathComponent("Refreshed", isDirectory: true)
|
||||
|
||||
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 = try FileManager.default.unzipAppBundle(at: installedApp.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) }
|
||||
|
||||
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
||||
|
||||
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
||||
"CFBundleURLName": installedApp.bundleIdentifier,
|
||||
"CFBundleURLSchemes": [installedApp.openAppURL.scheme!]] as [String : Any]
|
||||
allURLSchemes.append(altstoreURLScheme)
|
||||
|
||||
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
|
||||
signer.signApp(at: appBundleURL, provisioningProfile: provisioningProfile) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
|
||||
let resignedURL = try FileManager.default.zipAppBundle(at: appBundleURL)
|
||||
completionHandler(.success(resignedURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
context.perform {
|
||||
completionHandler(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func sendAppToServer(fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard let server = ServerManager.shared.discoveredServers.first else { return completionHandler(.failure(AppError.noServersFound)) }
|
||||
|
||||
server.installApp(at: fileURL, identifier: identifier) { (result) in
|
||||
let result = result.mapError { $0 as Error }
|
||||
completionHandler(result)
|
||||
}
|
||||
self.operationQueue.addOperation(authenticationOperation)
|
||||
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> (Progress, Progress)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == device.identifier })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ app: App, with team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let appName = app.name
|
||||
let bundleID = "com.\(team.identifier).\(app.identifier)"
|
||||
let context = installedApp.managedObjectContext
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(_ installedApp: InstalledApp, signer: ALTSigner, context: NSManagedObjectContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
self.prepareProvisioningProfile(for: installedApp.app, team: signer.team) { (result) in
|
||||
|
||||
// Install
|
||||
installAppOperation.addDependency(resignAppOperation)
|
||||
installAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let profile):
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
self.prepare(installedApp, provisioningProfile: profile, signer: signer) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let resignedURL):
|
||||
|
||||
// Send app to server
|
||||
installedApp.managedObjectContext?.perform {
|
||||
self.sendAppToServer(fileURL: resignedURL, identifier: installedApp.bundleIdentifier) { (result) in
|
||||
context.perform {
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
installedApp.expirationDate = profile.expirationDate
|
||||
completionHandler(.success(installedApp))
|
||||
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .success:
|
||||
context?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.operationQueue.addOperations([resignAppOperation, installAppOperation], waitUntilFinished: false)
|
||||
|
||||
return (resignAppOperation.progress, installAppOperation.progress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +76,19 @@
|
||||
<navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa">
|
||||
<barButtonItem key="rightBarButtonItem" title="Refresh All" id="0Ke-yl-tAg">
|
||||
<connections>
|
||||
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="6op-Mf-HSD"/>
|
||||
<action selector="refreshAllApps:" destination="Xf1-LZ-1vU" id="GOS-gx-qKD"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="progressView" destination="CuF-K7-fn8" id="SPP-VP-a9e"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="CuF-K7-fn8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</progressView>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1518" y="420"/>
|
||||
</scene>
|
||||
@@ -629,7 +636,7 @@
|
||||
<image name="second" width="30" height="30"/>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="miz-8X-gFg"/>
|
||||
<segue reference="8jj-zE-2hk"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Purple"/>
|
||||
</document>
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(InstalledApp)
|
||||
class InstalledApp: NSManagedObject
|
||||
class InstalledApp: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@@ -75,6 +75,13 @@ extension InstalledApp
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
class func refreshedIPAURL(for app: App) -> URL
|
||||
{
|
||||
let ipaURL = self.directoryURL(for: app).appendingPathComponent("Refreshed.ipa")
|
||||
return ipaURL
|
||||
}
|
||||
|
||||
|
||||
class func directoryURL(for app: App) -> URL
|
||||
{
|
||||
let directoryURL = InstalledApp.appsDirectoryURL.appendingPathComponent(app.identifier)
|
||||
@@ -92,4 +99,8 @@ extension InstalledApp
|
||||
var ipaURL: URL {
|
||||
return InstalledApp.ipaURL(for: self.app)
|
||||
}
|
||||
|
||||
var refreshedIPAURL: URL {
|
||||
return InstalledApp.refreshedIPAURL(for: self.app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
class MyAppsViewController: UITableViewController
|
||||
{
|
||||
private var refreshErrors = [String: Error]()
|
||||
@@ -22,12 +24,23 @@ class MyAppsViewController: UITableViewController
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
@IBOutlet private var progressView: UIProgressView!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
self.progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBar.addSubview(self.progressView)
|
||||
|
||||
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
|
||||
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
@@ -106,7 +119,9 @@ private extension MyAppsViewController
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
AppManager.shared.refreshAllApps(presentingViewController: self) { (result) in
|
||||
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
|
||||
|
||||
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
@@ -146,10 +161,49 @@ private extension MyAppsViewController
|
||||
self.refreshErrors = failures
|
||||
}
|
||||
|
||||
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.progressView.observedProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +237,7 @@ extension MyAppsViewController
|
||||
toastView.activityIndicatorView.startAnimating()
|
||||
toastView.show(in: self.navigationController?.view ?? self.view)
|
||||
|
||||
AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
||||
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -203,11 +257,16 @@ extension MyAppsViewController
|
||||
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)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.progressView.observedProgress = nil
|
||||
self.progressView.progress = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = progress
|
||||
}
|
||||
|
||||
return [deleteAction, refreshAction]
|
||||
|
||||
@@ -11,36 +11,27 @@ import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
extension AuthenticationOperation
|
||||
enum AuthenticationError: LocalizedError
|
||||
{
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
|
||||
case notAuthenticated
|
||||
case noTeam
|
||||
case noCertificate
|
||||
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
case noTeam
|
||||
case noCertificate
|
||||
|
||||
case missingPrivateKey
|
||||
case missingCertificate
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "")
|
||||
case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", comment: "")
|
||||
case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
|
||||
case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationOperation: RSTOperation
|
||||
@objc(AuthenticationOperation)
|
||||
class AuthenticationOperation: ResultOperation<ALTSigner>
|
||||
{
|
||||
var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)?
|
||||
|
||||
private weak var presentingViewController: UIViewController?
|
||||
|
||||
private lazy var navigationController = UINavigationController()
|
||||
@@ -48,106 +39,51 @@ class AuthenticationOperation: RSTOperation
|
||||
|
||||
private var appleIDPassword: String?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(presentingViewController: UIViewController?)
|
||||
{
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate")
|
||||
|
||||
func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>)
|
||||
{
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let (altTeam, altCertificate) = try result.get()
|
||||
let altAccount = altTeam.account
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
account.isActiveAccount = true
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||
|
||||
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||
for account in otherAccounts
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = Team(altTeam, account: account, context: context)
|
||||
team.isActiveTeam = true
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||
|
||||
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||
for team in otherTeams
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateIdentifier = altCertificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey
|
||||
|
||||
self.resultHandler?(.success((altTeam, altCertificate)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.resultHandler?(.failure(error))
|
||||
}
|
||||
|
||||
self.finish()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
RSTEndBackgroundTask(backgroundTaskID)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign In
|
||||
self.signIn { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let account):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Team
|
||||
self.fetchTeam(for: account) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let team):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
// Fetch Certificate
|
||||
self.fetchCertificate(for: team) { (result) in
|
||||
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let certificate): finish(.success((team, certificate)))
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let certificate):
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let signer = ALTSigner(team: team, certificate: certificate)
|
||||
self.finish(.success(signer))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +91,68 @@ class AuthenticationOperation: RSTOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<ALTSigner, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
print("Finished authenticating with result:", result)
|
||||
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
do
|
||||
{
|
||||
let signer = try result.get()
|
||||
let altAccount = signer.team.account
|
||||
|
||||
// Account
|
||||
let account = Account(altAccount, context: context)
|
||||
account.isActiveAccount = true
|
||||
|
||||
let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
|
||||
otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
|
||||
|
||||
let otherAccounts = try context.fetch(otherAccountsFetchRequest)
|
||||
for account in otherAccounts
|
||||
{
|
||||
account.isActiveAccount = false
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = Team(signer.team, account: account, context: context)
|
||||
team.isActiveTeam = true
|
||||
|
||||
let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
|
||||
otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
|
||||
|
||||
let otherTeams = try context.fetch(otherTeamsFetchRequest)
|
||||
for team in otherTeams
|
||||
{
|
||||
team.isActiveTeam = false
|
||||
}
|
||||
|
||||
// Save
|
||||
try context.save()
|
||||
|
||||
// Update keychain
|
||||
Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved.
|
||||
Keychain.shared.appleIDPassword = self.appleIDPassword
|
||||
|
||||
Keychain.shared.signingCertificateIdentifier = signer.certificate.identifier
|
||||
Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey
|
||||
|
||||
super.finish(.success(signer))
|
||||
}
|
||||
catch
|
||||
{
|
||||
super.finish(.failure(error))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.navigationController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AuthenticationOperation
|
||||
@@ -199,13 +197,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(authenticationViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.notAuthenticated))
|
||||
completionHandler(.failure(OperationError.notAuthenticated))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,13 +252,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(selectTeamViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noTeam))
|
||||
completionHandler(.failure(AuthenticationError.noTeam))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +292,7 @@ private extension AuthenticationOperation
|
||||
do
|
||||
{
|
||||
let certificate = try Result(certificate, error).get()
|
||||
guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey }
|
||||
guard let privateKey = certificate.privateKey else { throw AuthenticationError.missingPrivateKey }
|
||||
|
||||
ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in
|
||||
do
|
||||
@@ -302,7 +300,7 @@ private extension AuthenticationOperation
|
||||
let certificates = try Result(certificates, error).get()
|
||||
|
||||
guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else {
|
||||
throw Error.missingCertificate
|
||||
throw AuthenticationError.missingCertificate
|
||||
}
|
||||
|
||||
certificate.privateKey = privateKey
|
||||
@@ -334,13 +332,13 @@ private extension AuthenticationOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.failure(Error.cancelled))
|
||||
completionHandler(.failure(OperationError.cancelled))
|
||||
}
|
||||
}
|
||||
|
||||
if !self.present(replaceCertificateViewController)
|
||||
{
|
||||
completionHandler(.failure(Error.noCertificate))
|
||||
completionHandler(.failure(AuthenticationError.noCertificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
65
AltStore/Operations/DownloadAppOperation.swift
Normal file
65
AltStore/Operations/DownloadAppOperation.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// DownloadAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/10/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let app: App
|
||||
private let downloadURL: URL
|
||||
private let ipaURL: URL
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
|
||||
init(app: App)
|
||||
{
|
||||
self.app = app
|
||||
self.downloadURL = app.downloadURL
|
||||
self.ipaURL = InstalledApp.ipaURL(for: app)
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 1
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
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)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: self.app.objectID) as! App
|
||||
|
||||
let installedApp = InstalledApp(app: app,
|
||||
bundleIdentifier: app.identifier,
|
||||
expirationDate: Date(),
|
||||
context: context)
|
||||
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
catch let error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 1)
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
286
AltStore/Operations/InstallAppOperation.swift
Normal file
286
AltStore/Operations/InstallAppOperation.swift
Normal file
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// InstallAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(InstallAppOperation)
|
||||
class InstallAppOperation: ResultOperation<Void>
|
||||
{
|
||||
var fileURL: URL?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.altstore.InstallAppOperation")
|
||||
|
||||
private var connection: NWConnection?
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 4
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
guard let fileURL = self.fileURL else { return self.finish(.failure(OperationError.appNotFound)) }
|
||||
|
||||
// Connect to server.
|
||||
self.connect { (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) { (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
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
|
||||
{
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(server.service.name).", error)
|
||||
completionHandler(.failure(InstallationError.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, 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))
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
catch let error as ALTServerError
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch
|
||||
{
|
||||
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
|
||||
}
|
||||
}
|
||||
}
|
||||
84
AltStore/Operations/Operation.swift
Normal file
84
AltStore/Operations/Operation.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Operation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
class ResultOperation<ResultType>: Operation
|
||||
{
|
||||
var resultHandler: ((Result<ResultType, Error>) -> Void)?
|
||||
|
||||
@available(*, unavailable)
|
||||
override func finish()
|
||||
{
|
||||
super.finish()
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ResultType, Error>)
|
||||
{
|
||||
guard !self.isFinished else { return }
|
||||
|
||||
super.finish()
|
||||
|
||||
self.resultHandler?(result)
|
||||
}
|
||||
}
|
||||
|
||||
class Operation: RSTOperation, ProgressReporting
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
|
||||
private var backgroundTaskID: UIBackgroundTaskIdentifier?
|
||||
|
||||
override var isAsynchronous: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.progress.cancellationHandler = { [weak self] in self?.cancel() }
|
||||
}
|
||||
|
||||
override func cancel()
|
||||
{
|
||||
super.cancel()
|
||||
|
||||
if !self.progress.isCancelled
|
||||
{
|
||||
self.progress.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let name = "com.altstore." + NSStringFromClass(type(of: self))
|
||||
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: name) { [weak self] in
|
||||
guard let backgroundTask = self?.backgroundTaskID else { return }
|
||||
|
||||
self?.cancel()
|
||||
|
||||
UIApplication.shared.endBackgroundTask(backgroundTask)
|
||||
self?.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
|
||||
override func finish()
|
||||
{
|
||||
super.finish()
|
||||
|
||||
if let backgroundTaskID = self.backgroundTaskID
|
||||
{
|
||||
UIApplication.shared.endBackgroundTask(backgroundTaskID)
|
||||
self.backgroundTaskID = .invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
32
AltStore/Operations/OperationError.swift
Normal file
32
AltStore/Operations/OperationError.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// OperationError.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum OperationError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case unknownResult
|
||||
case cancelled
|
||||
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
|
||||
case unknownUDID
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
244
AltStore/Operations/ResignAppOperation.swift
Normal file
244
AltStore/Operations/ResignAppOperation.swift
Normal file
@@ -0,0 +1,244 @@
|
||||
//
|
||||
// ResignAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/7/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
@objc(ResignAppOperation)
|
||||
class ResignAppOperation: ResultOperation<URL>
|
||||
{
|
||||
let installedApp: InstalledApp
|
||||
private var context: NSManagedObjectContext?
|
||||
|
||||
var signer: ALTSigner?
|
||||
|
||||
init(installedApp: InstalledApp)
|
||||
{
|
||||
self.installedApp = installedApp
|
||||
self.context = installedApp.managedObjectContext
|
||||
|
||||
super.init()
|
||||
|
||||
self.progress.totalUnitCount = 3
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
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)) }
|
||||
|
||||
context.perform {
|
||||
// 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
|
||||
guard let appID = self.process(result) else { return }
|
||||
|
||||
// Fetch Provisioning Profile
|
||||
self.fetchProvisioningProfile(for: appID, team: signer.team) { (result) in
|
||||
guard let profile = self.process(result) else { return }
|
||||
|
||||
// Prepare app bundle
|
||||
context.perform {
|
||||
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
|
||||
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
|
||||
|
||||
let prepareAppBundleProgress = self.prepareAppBundle(for: self.installedApp) { (result) in
|
||||
guard let appBundleURL = self.process(result) else { return }
|
||||
|
||||
// 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 {
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: resignedURL, to: self.installedApp.refreshedIPAURL, shouldReplace: true)
|
||||
|
||||
let refreshedDirectory = resignedURL.deletingLastPathComponent()
|
||||
try? FileManager.default.removeItem(at: refreshedDirectory)
|
||||
|
||||
self.finish(.success(self.installedApp.refreshedIPAURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process<T>(_ result: Result<T, Error>) -> T?
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
self.finish(.failure(error))
|
||||
return nil
|
||||
|
||||
case .success(let value):
|
||||
guard !self.isCancelled else {
|
||||
self.finish(.failure(OperationError.cancelled))
|
||||
return nil
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ResignAppOperation
|
||||
{
|
||||
func registerCurrentDevice(for team: ALTTeam, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
|
||||
{
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
|
||||
return completionHandler(.failure(OperationError.unknownUDID))
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in
|
||||
do
|
||||
{
|
||||
let devices = try Result(devices, error).get()
|
||||
|
||||
if let device = devices.first(where: { $0.identifier == udid })
|
||||
{
|
||||
completionHandler(.success(device))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team) { (device, error) in
|
||||
completionHandler(Result(device, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func register(_ app: App, team: ALTTeam, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let appName = app.name
|
||||
let bundleID = "com.\(team.identifier).\(app.identifier)"
|
||||
|
||||
ALTAppleAPI.shared.fetchAppIDs(for: team) { (appIDs, error) in
|
||||
do
|
||||
{
|
||||
let appIDs = try Result(appIDs, error).get()
|
||||
|
||||
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleID })
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.addAppID(withName: appName, bundleIdentifier: bundleID, team: team) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
|
||||
{
|
||||
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team) { (profile, error) in
|
||||
completionHandler(Result(profile, error))
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAppBundle(for installedApp: InstalledApp, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||
{
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
// 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) }
|
||||
|
||||
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
||||
|
||||
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
||||
"CFBundleURLName": bundleIdentifier,
|
||||
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
|
||||
allURLSchemes.append(altstoreURLScheme)
|
||||
|
||||
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
|
||||
|
||||
completionHandler(.success(appBundleURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = signer.signApp(at: fileURL, provisioningProfile: profile) { (success, error) in
|
||||
do
|
||||
{
|
||||
try Result(success, error).get()
|
||||
|
||||
let ipaURL = try FileManager.default.zipAppBundle(at: fileURL)
|
||||
completionHandler(.success(ipaURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
//
|
||||
// Server.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/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 InstallError: LocalizedError
|
||||
{
|
||||
case unknown
|
||||
case cancelled
|
||||
case invalidApp
|
||||
case noUDID
|
||||
case server(ALTServerError)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .server(let error): return error.localizedDescription
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltStore.server", qos: .utility)
|
||||
|
||||
func installApp(at fileURL: URL, identifier: String, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
|
||||
{
|
||||
var isFinished = false
|
||||
|
||||
var serverConnection: NWConnection?
|
||||
|
||||
func finish(error: InstallError?)
|
||||
{
|
||||
// Prevent duplicate callbacks if connection is lost.
|
||||
guard !isFinished else { return }
|
||||
isFinished = true
|
||||
|
||||
if let connection = serverConnection
|
||||
{
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to install \(identifier).", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed \(identifier)!")
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
self.connect { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success(let connection):
|
||||
serverConnection = connection
|
||||
|
||||
self.sendApp(at: fileURL, via: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success:
|
||||
|
||||
self.receiveResponse(from: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: finish(error: nil)
|
||||
case .failure(let error): finish(error: .server(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Server
|
||||
{
|
||||
func connect(completionHandler: @escaping (Result<NWConnection, InstallError>) -> Void)
|
||||
{
|
||||
let connection = NWConnection(to: .service(name: self.service.name, type: self.service.type, domain: self.service.domain, interface: nil), using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { [weak service, unowned connection] (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: completionHandler(.success(connection))
|
||||
case .cancelled: completionHandler(.failure(.cancelled))
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(service?.name ?? "").", error)
|
||||
completionHandler(.failure(.server(.init(.connectionFailed))))
|
||||
|
||||
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, InstallError>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallError.invalidApp }
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw InstallError.noUDID }
|
||||
|
||||
let request = ServerRequest(udid: udid, contentSize: appData.count)
|
||||
let requestData = try JSONEncoder().encode(request)
|
||||
|
||||
let requestSize = Int32(requestData.count)
|
||||
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
|
||||
|
||||
// Send request data size.
|
||||
print("Sending request data size \(requestSize)")
|
||||
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send request.
|
||||
print("Sending request \(request)")
|
||||
connection.send(content: requestData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send app data.
|
||||
print("Sending app data (Size: \(appData.count))")
|
||||
connection.send(content: appData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
catch is EncodingError
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.invalidRequest))))
|
||||
}
|
||||
catch let error as InstallError
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
catch
|
||||
{
|
||||
assertionFailure("Unknown error type. \(error)")
|
||||
completionHandler(.failure(.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> 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)
|
||||
|
||||
if let error = response.error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +11,11 @@ import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
}
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
static let shared = ServerManager()
|
||||
|
||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 726e515ad9...c99aa73a89
Reference in New Issue
Block a user