FBDeviceControl/Commands/FBDeviceApplicationCommands.m (392 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBDeviceApplicationCommands.h" #import <objc/runtime.h> #import "FBAMDServiceConnection.h" #import "FBDevice+Private.h" #import "FBDevice.h" #import "FBDeviceControlError.h" #import "FBDeviceDebuggerCommands.h" #import "FBInstrumentsClient.h" @interface FBDeviceWorkflowStatistics : NSObject @property (nonatomic, copy, readonly) NSString *workflowType; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @property (nonatomic, copy, nullable, readwrite) NSDictionary<NSString *, id> *lastEvent; @end @implementation FBDeviceWorkflowStatistics - (instancetype)initWithWorkflowType:(NSString *)workflowType logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _workflowType = workflowType; _logger = logger; return self; } - (void)pushProgress:(NSDictionary<NSString *, id> *)event { [self.logger logFormat:@"%@ Progress: %@", self.workflowType, [FBCollectionInformation oneLineDescriptionFromDictionary:event]]; self.lastEvent = event; } - (NSString *)summaryOfRecentEvents { NSDictionary<NSString *, id> *lastEvent = self.lastEvent; if (!lastEvent) { return [NSString stringWithFormat:@"No events from %@", self.lastEvent]; } return [NSString stringWithFormat:@"Last event %@", [FBCollectionInformation oneLineDescriptionFromDictionary:lastEvent]]; } @end static void WorkflowCallback(NSDictionary<NSString *, id> *callbackDictionary, FBDeviceWorkflowStatistics *statistics) { [statistics pushProgress:callbackDictionary]; } @interface FBDeviceApplicationCommands () @property (nonatomic, weak, readonly) FBDevice *device; @property (nonatomic, copy, readonly) NSURL *deltaUpdateDirectory; - (FBFuture<NSNull *> *)killApplicationWithProcessIdentifier:(pid_t)processIdentifier; @end @interface FBDeviceLaunchedApplication : NSObject <FBLaunchedApplication> @property (nonatomic, strong, readonly) FBDeviceApplicationCommands *commands; @property (nonatomic, strong, readonly) dispatch_queue_t queue; @property (nonatomic, strong, readonly) FBApplicationLaunchConfiguration *configuration; @end @implementation FBDeviceLaunchedApplication @synthesize processIdentifier = _processIdentifier; - (instancetype)initWithProcessIdentifier:(pid_t)processIdentifier configuration:(FBApplicationLaunchConfiguration *)configuration commands:(FBDeviceApplicationCommands *)commands queue:(dispatch_queue_t)queue { self = [super init]; if (!self) { return nil; } _processIdentifier = processIdentifier; _configuration = configuration; _commands = commands; _queue = queue; return self; } - (FBFuture<NSNull *> *)applicationTerminated { FBDeviceApplicationCommands *commands = self.commands; pid_t processIdentifier = self.processIdentifier; return [FBMutableFuture.future onQueue:self.queue respondToCancellation:^ FBFuture<NSNull *> *{ return [commands killApplicationWithProcessIdentifier:processIdentifier]; }]; } - (NSString *)bundleID { return self.configuration.bundleID; } @end @implementation FBDeviceApplicationCommands #pragma mark Initializers + (instancetype)commandsWithTarget:(FBDevice *)target { NSURL *deltaUpdateDirectory = [target.temporaryDirectory temporaryDirectory]; return [[self alloc] initWithDevice:target deltaUpdateDirectory:deltaUpdateDirectory]; } - (instancetype)initWithDevice:(FBDevice *)device deltaUpdateDirectory:(NSURL *)deltaUpdateDirectory { self = [super init]; if (!self) { return nil; } _device = device; _deltaUpdateDirectory = deltaUpdateDirectory; return self; } #pragma mark FBApplicationCommands Implementation - (FBFuture<FBInstalledApplication *> *)installApplicationWithPath:(NSString *)path { // We need to get the bundle identifier of the installed application, in order that we can get install info later. NSError *error = nil; FBBundleDescriptor *bundle = [FBBundleDescriptor bundleFromPath:path error:&error]; if (!bundle) { return [FBFuture futureWithError:error]; } // Construct the options for the underlying install API. This mirrors as much of Xcode's call to the same API as is reasonable. // `@"PreferWifi": @1` may also be passed by Xcode. However, this being preferable is highly dependent on a fast WiFi network and both host/device on the same network. Since this is harder to pick a sane default for this option, this is omitted from the options. NSURL *appURL = [NSURL fileURLWithPath:path isDirectory:YES]; NSDictionary<NSString *, id> *options = @{ @"CFBundleIdentifier": bundle.identifier, // Lets the installer know what the Bundle ID is of the passed in artifact. @"CloseOnInvalidate": @1, // Standard arguments of lockdown services to ensure that the socket is closed on teardown. @"InvalidateOnDetach": @1, // Similar to the above. @"IsUserInitiated": @1, // Improves installation performance. This has a strong effect on time taken in "VerifyingApplication" stage of installation, which is CPU/IO bound on the attached device. @"PackageType": @"Developer", // Signifies that the passed payload is a .app @"ShadowParentKey": self.deltaUpdateDirectory, // Must be provided if 'Developer' is the 'PackageType'. Specifies where incremental install data and apps are persisted for faster future installs of the same bundle. }; // Perform the install and lookup the app after. return [[[self.device connectToDeviceWithPurpose:@"install"] onQueue:self.device.workQueue pop:^ FBFuture<NSNull *> * (id<FBDeviceCommands> device) { [self.device.logger logFormat:@"Installing Application %@", appURL]; // 'AMDeviceSecureInstallApplicationBundle' performs: // 1) The transfer of the application bundle to the device. // 2) The installation of the application after the transfer. // 3) The performing of the relevant delta updates in the directory pointed to by 'ShadowParentKey' FBDeviceWorkflowStatistics *statistics = [[FBDeviceWorkflowStatistics alloc] initWithWorkflowType:@"Install" logger:device.logger]; int status = device.calls.SecureInstallApplicationBundle( device.amDeviceRef, (__bridge CFURLRef _Nonnull)(appURL), (__bridge CFDictionaryRef _Nonnull)(options), (AMDeviceProgressCallback) WorkflowCallback, (__bridge void *) (statistics) ); if (status != 0) { NSString *errorMessage = CFBridgingRelease(device.calls.CopyErrorText(status)); return [[FBDeviceControlError describeFormat:@"Failed to install application %@ 0x%x (%@). %@", appURL.lastPathComponent, status, errorMessage, statistics.summaryOfRecentEvents] failFuture]; } [self.device.logger logFormat:@"Installed Application %@", appURL]; return FBFuture.empty; }] onQueue:self.device.asyncQueue fmap:^(id _) { return [self installedApplicationWithBundleID:bundle.identifier]; }]; } - (FBFuture<id> *)uninstallApplicationWithBundleID:(NSString *)bundleID { // It may be better to investigate if FB_AMDeviceSecureUninstallApplication // outputs some error message when the bundle id doesn't exist // Currently it returns 0 as if it had succeded // In case that's not possible, we should look into querying if // the app is installed first (FB_AMDeviceLookupApplications) return [[self.device connectToDeviceWithPurpose:@"uninstall_%@", bundleID] onQueue:self.device.workQueue pop:^ FBFuture<NSNull *> * (id<FBDeviceCommands> device) { FBDeviceWorkflowStatistics *statistics = [[FBDeviceWorkflowStatistics alloc] initWithWorkflowType:@"Install" logger:device.logger]; [self.device.logger logFormat:@"Uninstalling Application %@", bundleID]; int status = device.calls.SecureUninstallApplication( 0, device.amDeviceRef, (__bridge CFStringRef _Nonnull)(bundleID), 0, (AMDeviceProgressCallback) WorkflowCallback, (__bridge void *) (statistics) ); if (status != 0) { NSString *internalMessage = CFBridgingRelease(device.calls.CopyErrorText(status)); return [[FBDeviceControlError describeFormat:@"Failed to uninstall application '%@' with error 0x%x (%@). %@", bundleID, status, internalMessage, statistics.summaryOfRecentEvents] failFuture]; } [self.device.logger logFormat:@"Uninstalled Application %@", bundleID]; return FBFuture.empty; }]; } - (FBFuture<NSArray<FBInstalledApplication *> *> *)installedApplications { return [[self installedApplicationsData:FBDeviceApplicationCommands.installedApplicationLookupAttributes] onQueue:self.device.asyncQueue map:^(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *applicationData) { NSMutableArray<FBInstalledApplication *> *installedApplications = [[NSMutableArray alloc] initWithCapacity:applicationData.count]; NSEnumerator *objectEnumerator = [applicationData objectEnumerator]; for (NSDictionary *app in objectEnumerator) { if (app == nil) { continue; } FBInstalledApplication *application = [FBDeviceApplicationCommands installedApplicationFromDictionary:app]; [installedApplications addObject:application]; } return installedApplications; }]; } - (FBFuture<FBInstalledApplication *> *)installedApplicationWithBundleID:(NSString *)bundleID { return [[self installedApplicationsData:FBDeviceApplicationCommands.installedApplicationLookupAttributes] onQueue:self.device.asyncQueue fmap:^FBFuture *(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *applicationData) { NSDictionary<NSString *, id> *app = applicationData[bundleID]; if (!app) { return [[FBDeviceControlError describeFormat:@"Application with bundle ID: %@ is not installed. Installed apps %@ ", bundleID, [FBCollectionInformation oneLineDescriptionFromArray:applicationData.allKeys]] failFuture]; } FBInstalledApplication *application = [FBDeviceApplicationCommands installedApplicationFromDictionary:app]; return [FBFuture futureWithResult:application]; }]; } - (FBFuture<NSDictionary<NSString *, NSNumber *> *> *)runningApplications { return [[FBFuture futureWithFutures:@[ [self pidToRunningProcessName], [self installedApplicationsData:FBDeviceApplicationCommands.namingLookupAttributes], ]] onQueue:self.device.asyncQueue map:^ NSDictionary<NSString *, NSNumber *> * (NSArray<id> *tuple) { // Obtain the requested mappings. NSDictionary<NSNumber *, NSString *> *pidToRunningProcessName = tuple[0]; NSDictionary<NSString *, id> *bundleIdentifierToAttributes = tuple[1]; // Flip the mappings NSMutableDictionary<NSString *, NSString *> *bundleNameToBundleIdentifier = NSMutableDictionary.dictionary; for (NSString *bundleIdentifier in bundleIdentifierToAttributes.allKeys) { NSString *bundleName = bundleIdentifierToAttributes[bundleIdentifier][FBApplicationInstallInfoKeyBundleName]; bundleNameToBundleIdentifier[bundleName] = bundleIdentifier; } NSMutableDictionary<NSString *, NSNumber *> *runningProcessNameToPID = NSMutableDictionary.dictionary; for (NSNumber *processIdentifier in pidToRunningProcessName.allKeys) { NSString *processName = pidToRunningProcessName[processIdentifier]; runningProcessNameToPID[processName] = processIdentifier; } // Compare bundle names with PIDs by using the inverted mappings. NSMutableDictionary<NSString *, NSNumber *> *bundleNameToPID = NSMutableDictionary.dictionary; for (NSString *processName in runningProcessNameToPID.allKeys) { NSString *bundleName = bundleNameToBundleIdentifier[processName]; if (!bundleName) { continue; } NSNumber *pid = runningProcessNameToPID[processName]; bundleNameToPID[bundleName] = pid; } return bundleNameToPID; }]; } - (FBFuture<NSNumber *> *)processIDWithBundleID:(NSString *)bundleID { return [[self runningApplications] onQueue:self.device.asyncQueue fmap:^(NSDictionary<NSString *, NSNumber *> *result) { NSNumber *pid = result[bundleID]; if (!pid) { return [[FBDeviceControlError describeFormat:@"No pid for %@", bundleID] failFuture]; } return [FBFuture futureWithResult:pid]; }]; } - (FBFuture<NSNull *> *)killApplicationWithBundleID:(NSString *)bundleID { return [[self processIDWithBundleID:bundleID] onQueue:self.device.workQueue fmap:^(NSNumber *processIdentifier) { return [self killApplicationWithProcessIdentifier:processIdentifier.intValue]; }]; } - (FBFuture<id<FBLaunchedApplication>> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration { return [[[self remoteInstrumentsClient] onQueue:self.device.asyncQueue pop:^(FBInstrumentsClient *client) { return [client launchApplication:configuration]; }] onQueue:self.device.asyncQueue map:^ id<FBLaunchedApplication> (NSNumber *pid) { return [[FBDeviceLaunchedApplication alloc] initWithProcessIdentifier:pid.intValue configuration:configuration commands:self queue:self.device.workQueue]; }]; } #pragma mark Private - (FBFuture<NSNull *> *)killApplicationWithProcessIdentifier:(pid_t)processIdentifier { return [[self remoteInstrumentsClient] onQueue:self.device.asyncQueue pop:^(FBInstrumentsClient *client) { return [client killProcess:processIdentifier]; }]; } - (FBFuture<NSDictionary<NSString *, NSDictionary<NSString *, id> *> *> *)installedApplicationsData:(NSArray<NSString *> *)returnAttributes { return [[self.device connectToDeviceWithPurpose:@"installed_apps"] onQueue:self.device.workQueue pop:^ FBFuture<NSDictionary<NSString *, NSDictionary<NSString *, id> *> *> * (id<FBDeviceCommands> device) { NSDictionary<NSString *, id> *options = @{ @"ReturnAttributes": returnAttributes, }; CFDictionaryRef applications; int status = device.calls.LookupApplications( device.amDeviceRef, (__bridge CFDictionaryRef _Nullable)(options), &applications ); if (status != 0) { NSString *errorMessage = CFBridgingRelease(device.calls.CopyErrorText(status)); return [[FBDeviceControlError describeFormat:@"Failed to get list of applications 0x%x (%@)", status, errorMessage] failFuture]; } return [FBFuture futureWithResult:CFBridgingRelease(applications)]; }]; } - (FBFutureContext<FBInstrumentsClient *> *)remoteInstrumentsClient { // There is a change in service names in iOS 14 that we have to account for. // Both of these channels are fine to use with the same underlying protocol, so long as the secure wrapper is used on the transport. BOOL usesSecureConnection = self.device.osVersion.version.majorVersion >= 14; return [[[self.device ensureDeveloperDiskImageIsMounted] onQueue:self.device.workQueue pushTeardown:^(id _) { return [self.device startService:(usesSecureConnection ? @"com.apple.instruments.remoteserver.DVTSecureSocketProxy" : @"com.apple.instruments.remoteserver")]; }] onQueue:self.device.asyncQueue pend:^(FBAMDServiceConnection *connection) { return [FBInstrumentsClient instrumentsClientWithServiceConnection:connection logger:self.device.logger]; }]; } - (FBFuture<NSDictionary<NSString *, NSNumber *> *> *)pidToRunningProcessName { return [[self.device startService:@"com.apple.os_trace_relay"] onQueue:self.device.asyncQueue pop:^(FBAMDServiceConnection *connection) { NSError *error = nil; BOOL success = [connection sendMessage:@{@"Request": @"PidList"} error:&error]; if (!success) { return [[FBDeviceControlError describeFormat:@"Failed to request PidList %@", error] failFuture]; } NSData *data = [connection receive:1 error:&error]; if (!data) { return [[FBDeviceControlError describeFormat:@"Failed to receive 1 byte after PidList %@", error] failFuture]; } NSDictionary<NSString *, id> *response = [connection receiveMessageWithError:&error]; if (!response) { return [[FBDeviceControlError describeFormat:@"Failed to receive PidList response %@", error] failFuture]; } NSString *status = response[@"Status"]; if (![status isEqualToString:@"RequestSuccessful"]) { return [[FBDeviceControlError describeFormat:@"Request to PidList is not RequestSuccessful %@", error] failFuture]; } NSDictionary<NSNumber *, id> *payload = response[@"Payload"]; NSMutableDictionary<NSNumber *, NSString *> *pidToRunningProcessName = NSMutableDictionary.dictionary; for (NSNumber *processIdentifer in payload.keyEnumerator) { NSDictionary<NSString *, NSString *> *contents = payload[processIdentifer]; NSString *processName = contents[@"ProcessName"]; if (![processName isKindOfClass:NSString.class]) { continue; } pidToRunningProcessName[processIdentifer] = processName; } return [FBFuture futureWithResult:pidToRunningProcessName]; }]; } + (FBInstalledApplication *)installedApplicationFromDictionary:(NSDictionary<NSString *, id> *)app { NSString *bundleName = app[FBApplicationInstallInfoKeyBundleName] ?: @""; NSString *path = app[FBApplicationInstallInfoKeyPath] ?: @""; NSString *bundleID = app[FBApplicationInstallInfoKeyBundleIdentifier]; FBBundleDescriptor *bundle = [[FBBundleDescriptor alloc] initWithName:bundleName identifier:bundleID path:path binary:nil]; return [FBInstalledApplication installedApplicationWithBundle:bundle installTypeString:(app[FBApplicationInstallInfoKeyApplicationType] ?: @"") signerIdentity:(app[FBApplicationInstallInfoKeySignerIdentity] ? : @"") dataContainer:nil]; } + (NSArray<NSString *> *)installedApplicationLookupAttributes { static dispatch_once_t onceToken; static NSArray<NSString *> *lookupAttributes = nil; dispatch_once(&onceToken, ^{ lookupAttributes = @[ FBApplicationInstallInfoKeyApplicationType, FBApplicationInstallInfoKeyBundleIdentifier, FBApplicationInstallInfoKeyBundleName, FBApplicationInstallInfoKeyPath, FBApplicationInstallInfoKeySignerIdentity, ]; }); return lookupAttributes; } + (NSArray<NSString *> *)namingLookupAttributes { static dispatch_once_t onceToken; static NSArray<NSString *> *lookupAttributes = nil; dispatch_once(&onceToken, ^{ lookupAttributes = @[ FBApplicationInstallInfoKeyBundleIdentifier, FBApplicationInstallInfoKeyBundleName, ]; }); return lookupAttributes; } @end