mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 03:33:36 +01:00
Installs apps to connected devices
This commit is contained in:
@@ -6,16 +6,29 @@
|
|||||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
#import <AltSign/AltSign.h>
|
#import <AltSign/AltSign.h>
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
extern NSErrorDomain const ALTDeviceErrorDomain;
|
||||||
|
|
||||||
|
typedef NS_ERROR_ENUM(ALTDeviceErrorDomain, ALTDeviceError)
|
||||||
|
{
|
||||||
|
ALTDeviceErrorUnknown,
|
||||||
|
ALTDeviceErrorNotConnected,
|
||||||
|
ALTDeviceErrorConnectionFailed,
|
||||||
|
ALTDeviceErrorWriteFailed,
|
||||||
|
};
|
||||||
|
|
||||||
@interface ALTDeviceManager : NSObject
|
@interface ALTDeviceManager : NSObject
|
||||||
|
|
||||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
||||||
|
|
||||||
|
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDevice:(ALTDevice *)altDevice completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|||||||
@@ -10,6 +10,21 @@
|
|||||||
|
|
||||||
#include <libimobiledevice/libimobiledevice.h>
|
#include <libimobiledevice/libimobiledevice.h>
|
||||||
#include <libimobiledevice/lockdown.h>
|
#include <libimobiledevice/lockdown.h>
|
||||||
|
#include <libimobiledevice/installation_proxy.h>
|
||||||
|
#include <libimobiledevice/notification_proxy.h>
|
||||||
|
#include <libimobiledevice/afc.h>
|
||||||
|
|
||||||
|
void ALTDeviceManagerDidFinishAppInstallation(const char *notification, void *udid);
|
||||||
|
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid);
|
||||||
|
|
||||||
|
NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError";
|
||||||
|
|
||||||
|
@interface ALTDeviceManager ()
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(void)> *installationCompletionHandlers;
|
||||||
|
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation ALTDeviceManager
|
@implementation ALTDeviceManager
|
||||||
|
|
||||||
@@ -24,6 +39,318 @@
|
|||||||
return _manager;
|
return _manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (instancetype)init
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (self)
|
||||||
|
{
|
||||||
|
_installationCompletionHandlers = [NSMutableDictionary dictionary];
|
||||||
|
_installationProgress = [NSMutableDictionary dictionary];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDevice:(ALTDevice *)altDevice completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||||
|
{
|
||||||
|
NSProgress *progress = [NSProgress discreteProgressWithTotalUnitCount:100];
|
||||||
|
|
||||||
|
NSUUID *UUID = [NSUUID UUID];
|
||||||
|
char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
|
||||||
|
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
free(uuidString);
|
||||||
|
|
||||||
|
if (error != nil)
|
||||||
|
{
|
||||||
|
completionHandler(NO, error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(YES, nil);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NSURL *appBundleURL = nil;
|
||||||
|
NSURL *temporaryDirectoryURL = nil;
|
||||||
|
|
||||||
|
if ([fileURL.pathExtension.lowercaseString isEqualToString:@"app"])
|
||||||
|
{
|
||||||
|
appBundleURL = fileURL;
|
||||||
|
temporaryDirectoryURL = nil;
|
||||||
|
}
|
||||||
|
else if ([fileURL.pathExtension.lowercaseString isEqualToString:@"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])
|
||||||
|
{
|
||||||
|
finish(error);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
appBundleURL = [[NSFileManager defaultManager] unzipAppBundleAtURL:fileURL toDirectory:temporaryDirectoryURL error:&error];
|
||||||
|
if (appBundleURL == nil)
|
||||||
|
{
|
||||||
|
finish(error);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: fileURL}]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find Device */
|
||||||
|
if (idevice_new(&device, altDevice.identifier.UTF8String) != IDEVICE_E_SUCCESS)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorNotConnected userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connect to Device */
|
||||||
|
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed 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:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (np_client_new(device, service, &np) != NP_E_SUCCESS)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
np_set_notify_callback(np, ALTDeviceManagerDidFinishAppInstallation, uuidString);
|
||||||
|
|
||||||
|
const char *notifications[3] = { NP_APP_INSTALLED, NP_APP_UNINSTALLED, 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)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed 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:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockdownd_client_free(client);
|
||||||
|
client = NULL;
|
||||||
|
|
||||||
|
if (afc_client_new(device, service, &afc) != AFC_E_SUCCESS)
|
||||||
|
{
|
||||||
|
finish([NSError errorWithDomain:ALTDeviceErrorDomain code:ALTDeviceErrorConnectionFailed 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:ALTDeviceErrorDomain code:ALTDeviceErrorWriteFailed userInfo:nil]);
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
while (files[i])
|
||||||
|
{
|
||||||
|
free(files[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.installationProgress[UUID] = progress;
|
||||||
|
self.installationCompletionHandlers[UUID] = ^{
|
||||||
|
finish(nil);
|
||||||
|
|
||||||
|
if (temporaryDirectoryURL != nil)
|
||||||
|
{
|
||||||
|
NSError *error = nil;
|
||||||
|
if (![[NSFileManager defaultManager] removeItemAtURL:temporaryDirectoryURL error:&error])
|
||||||
|
{
|
||||||
|
NSLog(@"Error removing temporary directory. %@", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
afc_make_directory(afc, destinationURL.relativePath.fileSystemRepresentation);
|
||||||
|
|
||||||
|
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtURL:directoryURL
|
||||||
|
includingPropertiesForKeys:@[NSURLIsDirectoryKey]
|
||||||
|
options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
|
||||||
|
errorHandler:^BOOL(NSURL * _Nonnull url, NSError * _Nonnull error) {
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"[Error] %@ (%@)", error, url);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (NSURL *fileURL in enumerator)
|
||||||
|
{
|
||||||
|
NSNumber *isDirectory = nil;
|
||||||
|
if (![fileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:error])
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([isDirectory boolValue])
|
||||||
|
{
|
||||||
|
NSURL *destinationDirectoryURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:YES];
|
||||||
|
if (![self writeDirectory:fileURL toDestinationURL:destinationDirectoryURL client:afc error:error])
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NSURL *destinationFileURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:NO];
|
||||||
|
if (![self writeFile:fileURL toDestinationURL:destinationFileURL client:afc error:error])
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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)
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t af = 0;
|
||||||
|
if ((afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || af == 0)
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (error)
|
||||||
|
{
|
||||||
|
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
|
||||||
|
}
|
||||||
|
|
||||||
|
success = NO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesWritten += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesWritten != data.length)
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
|
||||||
|
}
|
||||||
|
|
||||||
|
success = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
afc_file_close(afc, af);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Getters -
|
#pragma mark - Getters -
|
||||||
|
|
||||||
- (NSArray<ALTDevice *> *)connectedDevices
|
- (NSArray<ALTDevice *> *)connectedDevices
|
||||||
@@ -94,3 +421,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
#pragma mark - Callbacks -
|
||||||
|
|
||||||
|
void ALTDeviceManagerDidFinishAppInstallation(const char *notification, void *udid)
|
||||||
|
{
|
||||||
|
NSUUID *UUID = [[NSUUID alloc] initWithUUIDString:[NSString stringWithUTF8String:(const char *)udid]];
|
||||||
|
|
||||||
|
void (^completionHandler)(void) = ALTDeviceManager.sharedManager.installationCompletionHandlers[UUID];
|
||||||
|
if (completionHandler != nil)
|
||||||
|
{
|
||||||
|
completionHandler();
|
||||||
|
ALTDeviceManager.sharedManager.installationCompletionHandlers[UUID] = nil;
|
||||||
|
ALTDeviceManager.sharedManager.installationProgress[UUID] = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid)
|
||||||
|
{
|
||||||
|
NSUUID *UUID = [[NSUUID alloc] initWithUUIDString:[NSString stringWithUTF8String:(const char *)udid]];
|
||||||
|
|
||||||
|
NSProgress *progress = ALTDeviceManager.sharedManager.installationProgress[UUID];
|
||||||
|
if (progress == nil)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int percent = -1;
|
||||||
|
instproxy_status_get_percent_complete(status, &percent);
|
||||||
|
|
||||||
|
if (progress.completedUnitCount < percent)
|
||||||
|
{
|
||||||
|
progress.completedUnitCount = percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user