idb_companion/Server/FBIDBCommandExecutor.m (866 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 "FBIDBCommandExecutor.h" #import <FBSimulatorControl/FBSimulatorControl.h> #import <FBDeviceControl/FBDeviceControl.h> #import "FBXCTestDescriptor.h" #import "FBXCTestRunRequest.h" #import "FBXCTestRunRequest.h" #import "FBIDBStorageManager.h" #import "FBIDBError.h" #import "FBIDBLogger.h" #import "FBIDBPortsConfiguration.h" #import "FBDsymInstallLinkToBundle.h" FBFileContainerKind const FBFileContainerKindXctest = @"xctest"; FBFileContainerKind const FBFileContainerKindDylib = @"dylib"; FBFileContainerKind const FBFileContainerKindDsym = @"dsym"; FBFileContainerKind const FBFileContainerKindFramework = @"framework"; @interface FBIDBCommandExecutor () @property (nonatomic, strong, readonly) id<FBiOSTarget> target; @property (nonatomic, strong, readonly) FBIDBLogger *logger; @property (nonatomic, strong, readonly) FBIDBPortsConfiguration *ports; @end @implementation FBIDBCommandExecutor #pragma mark Initializers + (instancetype)commandExecutorForTarget:(id<FBiOSTarget>)target storageManager:(FBIDBStorageManager *)storageManager temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory ports:(FBIDBPortsConfiguration *)ports logger:(FBIDBLogger *)logger { return [[self alloc] initWithTarget:target storageManager:storageManager temporaryDirectory:temporaryDirectory ports:ports logger:[logger withName:@"grpc_handler"]]; } - (instancetype)initWithTarget:(id<FBiOSTarget>)target storageManager:(FBIDBStorageManager *)storageManager temporaryDirectory:(FBTemporaryDirectory *)temporaryDirectory ports:(FBIDBPortsConfiguration *)ports logger:(FBIDBLogger *)logger { self = [super init]; if (!self) { return nil; } _target = target; _storageManager = storageManager; _temporaryDirectory = temporaryDirectory; _ports = ports; _logger = logger; return self; } #pragma mark Installation - (FBFuture<NSDictionary<FBInstalledApplication *, id> *> *)list_apps:(BOOL)fetchProcessState { return [[FBFuture futureWithFutures:@[ [self.target installedApplications], fetchProcessState ? [self.target runningApplications] : [FBFuture futureWithResult:@{}], ]] onQueue:self.target.workQueue map:^(NSArray<id> *results) { NSArray<FBInstalledApplication *> *installed = results[0]; NSDictionary<NSString *, NSNumber *> *running = results[1]; NSMutableDictionary<FBInstalledApplication *, id> *listing = NSMutableDictionary.dictionary; for (FBInstalledApplication *application in installed) { listing[application] = running[application.bundle.identifier] ?: NSNull.null; } return listing; }]; } - (FBFuture<FBInstalledArtifact *> *)install_app_file_path:(NSString *)filePath make_debuggable:(BOOL)makeDebuggable { // Use .app directly, or extract an .ipa if ([FBBundleDescriptor isApplicationAtPath:filePath]) { return [self installAppBundle:[FBFutureContext futureContextWithFuture:[FBBundleDescriptor extractedApplicationAtPath:filePath]] makeDebuggable:makeDebuggable]; } else { return [self installExtractedApp:[self.temporaryDirectory withArchiveExtractedFromFile:filePath] makeDebuggable:makeDebuggable]; } } - (FBFuture<FBInstalledArtifact *> *)install_app_stream:(FBProcessInput *)input compression:(FBCompressionFormat)compression make_debuggable:(BOOL)makeDebuggable { return [self installExtractedApp:[self.temporaryDirectory withArchiveExtractedFromStream:input compression:compression] makeDebuggable:makeDebuggable]; } - (FBFuture<FBInstalledArtifact *> *)install_xctest_app_file_path:(NSString *)filePath { return [self installXctestFilePath:[FBFutureContext futureContextWithFuture:[FBFuture futureWithResult:[NSURL fileURLWithPath:filePath]]]]; } - (FBFuture<FBInstalledArtifact *> *)install_xctest_app_stream:(FBProcessInput *)stream { return [self installXctest:[self.temporaryDirectory withArchiveExtractedFromStream:stream compression:FBCompressionFormatGZIP]]; } - (FBFuture<FBInstalledArtifact *> *)install_dylib_file_path:(NSString *)filePath { return [self installFile:[FBFutureContext futureContextWithFuture:[FBFuture futureWithResult:[NSURL fileURLWithPath:filePath]]] intoStorage:self.storageManager.dylib]; } - (FBFuture<FBInstalledArtifact *> *)install_dylib_stream:(FBProcessInput *)input name:(NSString *)name { return [self installFile:[self.temporaryDirectory withGzipExtractedFromStream:input name:name] intoStorage:self.storageManager.dylib]; } - (FBFuture<FBInstalledArtifact *> *)install_framework_file_path:(NSString *)filePath { return [self installBundle:[FBFutureContext futureContextWithFuture:[FBFuture futureWithResult:[NSURL fileURLWithPath:filePath]]] intoStorage:self.storageManager.framework]; } - (FBFuture<FBInstalledArtifact *> *)install_framework_stream:(FBProcessInput *)input { return [self installBundle:[self.temporaryDirectory withArchiveExtractedFromStream:input compression:FBCompressionFormatGZIP] intoStorage:self.storageManager.framework]; } - (FBFuture<FBInstalledArtifact *> *)install_dsym_file_path:(NSString *)filePath linkTo:(nullable FBDsymInstallLinkToBundle *)linkTo { return [self installAndLinkDsym:[FBFutureContext futureContextWithFuture:[FBFuture futureWithResult:[NSURL fileURLWithPath:filePath]]] intoStorage:self.storageManager.dsym linkTo:linkTo]; } - (FBFuture<FBInstalledArtifact *> *)install_dsym_stream:(FBProcessInput *)input compression:(FBCompressionFormat)compression linkTo:(nullable FBDsymInstallLinkToBundle *)linkTo { return [self installAndLinkDsym:[self dsymDirnameFromUnzipDir:[self.temporaryDirectory withArchiveExtractedFromStream:input compression:compression]] intoStorage:self.storageManager.dsym linkTo:linkTo]; } #pragma mark Public Methods - (FBFuture<NSData *> *)take_screenshot:(FBScreenshotFormat)format { return [[self screenshotCommands] onQueue:self.target.workQueue fmap:^(id<FBScreenshotCommands> commands) { return [commands takeScreenshot:format]; }]; } - (FBFuture<NSArray<NSDictionary<NSString *, id> *> *> *)accessibility_info_at_point:(nullable NSValue *)value nestedFormat:(BOOL)nestedFormat { return [[self accessibilityCommands] onQueue:self.target.workQueue fmap:^ FBFuture * (id<FBAccessibilityCommands> commands) { if (value) { return [commands accessibilityElementAtPoint:value.pointValue nestedFormat:nestedFormat]; } else { return [commands accessibilityElementsWithNestedFormat:nestedFormat]; } }]; } - (FBFuture<NSNull *> *)add_media:(NSArray<NSURL *> *)filePaths { return [self.mediaCommands onQueue:self.target.asyncQueue fmap:^FBFuture *(id<FBSimulatorMediaCommands> commands) { return [commands addMedia:filePaths]; }]; } - (FBFuture<NSNull *> *)set_location:(double)latitude longitude:(double)longitude { id<FBLocationCommands> commands = (id<FBLocationCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBLocationCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBLocationCommands", commands] failFuture]; } return [commands overrideLocationWithLongitude:longitude latitude:latitude]; } - (FBFuture<NSNull *> *)clear_keychain { return [self.keychainCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorKeychainCommands> commands) { return [commands clearKeychain]; }]; } - (FBFuture<NSNull *> *)approve:(NSSet<FBSettingsApprovalService> *)services for_application:(NSString *)bundleID { return [self.settingsCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorSettingsCommands> commands) { return [commands grantAccess:[NSSet setWithObject:bundleID] toServices:services]; }]; } - (FBFuture<NSNull *> *)approve_deeplink:(NSString *)scheme for_application:(NSString *)bundleID { return [self.settingsCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorSettingsCommands> commands) { return [commands grantAccess:[NSSet setWithObject:bundleID] toDeeplink:scheme]; }]; } - (FBFuture<NSNull *> *)open_url:(NSString *)url { return [self.lifecycleCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorLifecycleCommands> commands) { return [commands openURL:[NSURL URLWithString:url]]; }]; } - (FBFuture<NSNull *> *)remove_all_storage_and_clear_keychain { NSError *error = nil; if (![self.storageManager clean:&error]) { return [FBFuture futureWithError:error]; } return [self clear_keychain]; } - (FBFuture<NSNull *> *)uninstall_all_applications { return [[self list_apps:NO] onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> *(NSDictionary<FBInstalledApplication *,id> *apps) { NSMutableArray<FBFuture<NSNull *> *> *uninstall_futures = NSMutableArray.array; for (FBInstalledApplication *app in apps) { if (app.installType == FBApplicationInstallTypeUser){ [uninstall_futures addObject:[[self kill_application:app.bundle.identifier] onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> *(id _) { return [self uninstall_application:app.bundle.identifier]; }]]; } } return [FBFuture futureWithFutures:uninstall_futures]; }]; } - (FBFuture<NSNull *> *)clean { if (self.target.state == FBiOSTargetStateShutdown) { return [self remove_all_storage_and_clear_keychain]; } return [[self uninstall_all_applications] onQueue:self.target.workQueue fmap:^FBFuture<NSNull *> *(id _) { return [self remove_all_storage_and_clear_keychain]; }]; } - (FBFuture<NSNull *> *)focus { return [self.lifecycleCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorLifecycleCommands> commands) { return [commands focus]; }]; } - (FBFuture<NSNull *> *)update_contacts:(NSData *)dbTarData { return [[self.temporaryDirectory withArchiveExtracted:dbTarData] onQueue:self.target.workQueue pop:^(NSURL *tempDirectory) { return [self.settingsCommands onQueue:self.target.workQueue fmap:^FBFuture *(id<FBSimulatorSettingsCommands> commands) { return [commands updateContacts:tempDirectory.path]; }]; }]; } - (FBFuture<NSSet<id<FBXCTestDescriptor>> *> *)list_test_bundles { return [FBFuture onQueue:self.target.workQueue resolve:^{ NSError *error; NSSet<id<FBXCTestDescriptor>> *testDescriptors = [self.storageManager.xctest listTestDescriptorsWithError:&error]; if (testDescriptors == nil) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:testDescriptors]; }]; } static const NSTimeInterval ListTestBundleTimeout = 60.0; - (FBFuture<NSArray<NSString *> *> *)list_tests_in_bundle:(NSString *)bundleID with_app:(NSString *)appPath { if ([appPath isEqualToString:@""]) { appPath = nil; } if ([self.storageManager.application.persistedBundleIDs containsObject:appPath]) { // appPath is actually an app bundle ID appPath = self.storageManager.application.persistedBundles[appPath].path; } return [FBFuture onQueue:self.target.workQueue resolve:^ FBFuture<NSArray<NSString *> *> * { NSError *error = nil; id<FBXCTestDescriptor> testDescriptor = [self.storageManager.xctest testDescriptorWithID:bundleID error:&error]; if (!testDescriptor) { return [FBFuture futureWithError:error]; } id<FBXCTestExtendedCommands> commands = (id<FBXCTestExtendedCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBXCTestExtendedCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBXCTestExtendedCommands", commands] failFuture]; } return [commands listTestsForBundleAtPath:testDescriptor.url.path timeout:ListTestBundleTimeout withAppAtPath:appPath]; }]; } - (FBFuture<NSNull *> *)uninstall_application:(NSString *)bundleID { return [self.target uninstallApplicationWithBundleID:bundleID]; } - (FBFuture<NSNull *> *)kill_application:(NSString *)bundleID { return [self.target killApplicationWithBundleID:bundleID]; } - (FBFuture<id<FBLaunchedApplication>> *)launch_app:(FBApplicationLaunchConfiguration *)configuration { NSMutableDictionary<NSString *, NSString *> *replacements = NSMutableDictionary.dictionary; [replacements addEntriesFromDictionary:self.storageManager.replacementMapping]; [replacements addEntriesFromDictionary:self.target.replacementMapping]; NSDictionary<NSString *, NSString *> *environment = [self applyEnvironmentReplacements:configuration.environment replacements:replacements]; FBApplicationLaunchConfiguration *derived = [[FBApplicationLaunchConfiguration alloc] initWithBundleID:configuration.bundleID bundleName:configuration.bundleName arguments:configuration.arguments environment:environment waitForDebugger:configuration.waitForDebugger io:configuration.io launchMode:configuration.launchMode]; return [self.target launchApplication:derived]; } - (NSDictionary<NSString *, NSString *> *)applyEnvironmentReplacements:(NSDictionary<NSString *, NSString *> *)environment replacements:(NSDictionary<NSString *, NSString *> *)replacements { [self.logger logFormat:@"Original environment: %@", environment]; [self.logger logFormat:@"Existing replacement mapping: %@", replacements]; NSMutableDictionary<NSString *, NSString *> *interpolatedEnvironment = [NSMutableDictionary dictionaryWithCapacity:environment.count]; for (NSString *name in environment.allKeys) { NSString *value = environment[name]; for (NSString *interpolationName in replacements.allKeys) { NSString *interpolationValue = replacements[interpolationName]; value = [value stringByReplacingOccurrencesOfString:interpolationName withString:interpolationValue]; } interpolatedEnvironment[name] = value; } [self.logger logFormat:@"Interpolated environment: %@", interpolatedEnvironment]; return interpolatedEnvironment; } - (FBFuture<FBIDBTestOperation *> *)xctest_run:(FBXCTestRunRequest *)request reporter:(id<FBXCTestReporter>)reporter logger:(id<FBControlCoreLogger>)logger { return [request startWithBundleStorageManager:self.storageManager.xctest target:self.target reporter:reporter logger:logger temporaryDirectory:self.temporaryDirectory]; } - (FBFuture<id<FBDebugServer>> *)debugserver_start:(NSString *)bundleID { id<FBDebuggerCommands> commands = (id<FBDebuggerCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBDebuggerCommands)]) { return [[FBControlCoreError describeFormat:@"Target doesn't conform to FBDebuggerCommands protocol %@", commands] failFuture]; } return [[[self debugserver_prepare:bundleID] onQueue:self.target.workQueue fmap:^(FBBundleDescriptor *application) { return [commands launchDebugServerForHostApplication:application port:self.ports.debugserverPort]; }] onQueue:self.target.workQueue doOnResolved:^(id<FBDebugServer> debugServer) { self.debugServer = debugServer; }]; } - (FBFuture<id<FBDebugServer>> *)debugserver_status { return [FBFuture onQueue:self.target.workQueue resolve:^{ id<FBDebugServer> debugServer = self.debugServer; if (!debugServer) { return [[FBControlCoreError describe:@"No debug server running"] failFuture]; } return [FBFuture futureWithResult:debugServer]; }]; } - (FBFuture<id<FBDebugServer>> *)debugserver_stop { return [[[self debugserver_status] onQueue:self.target.workQueue fmap:^(id<FBDebugServer> debugServer) { return [[self.debugServer.completed cancel] mapReplace:debugServer]; }] onQueue:self.target.workQueue doOnResolved:^(id _) { self.debugServer = nil; }]; } - (FBFuture<NSArray<FBCrashLogInfo *> *> *)crash_list:(NSPredicate *)predicate { return [[self.target crashes:predicate useCache:NO] onQueue:self.target.asyncQueue map:^(NSArray<FBCrashLogInfo *> *crashes) { return crashes; }]; } - (FBFuture<FBCrashLog *> *)crash_show:(NSPredicate *)predicate { return [[self.target crashes:predicate useCache:YES] onQueue:self.target.asyncQueue fmap:^(NSArray<FBCrashLogInfo *> *crashes) { if (crashes.count > 1) { return [[FBIDBError describeFormat:@"More than one crash log matching %@", predicate] failFuture]; } if (crashes.count == 0) { return [[FBIDBError describeFormat:@"No crashes matching %@", predicate] failFuture]; } NSError *error = nil; FBCrashLog *log = [crashes.firstObject obtainCrashLogWithError:&error]; if (!log) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:log]; }]; } - (FBFuture<NSArray<FBCrashLogInfo *> *> *)crash_delete:(NSPredicate *)predicate { return [[self.target pruneCrashes:predicate] onQueue:self.target.asyncQueue map:^(NSArray<FBCrashLogInfo *> *crashes) { return crashes; }]; } - (FBFuture<FBBundleDescriptor *> *)debugserver_prepare:(NSString *)bundleID { return [FBFuture onQueue:self.target.workQueue resolve:^ FBFuture<FBBundleDescriptor *> * { if (self.debugServer) { return [[FBControlCoreError describeFormat:@"Debug server is already running"] failFuture]; } NSDictionary<NSString *, FBBundleDescriptor *> *persisted = self.storageManager.application.persistedBundles; FBBundleDescriptor *bundle = persisted[bundleID]; if (!bundle) { return [[FBIDBError describeFormat:@"%@ not persisted application and is therefore not debuggable. Suitable applications: %@", bundleID, [FBCollectionInformation oneLineDescriptionFromArray:persisted.allKeys]] failFuture]; } return [FBFuture futureWithResult:bundle]; }]; } - (FBFuture<id<FBLogOperation>> *)tail_companion_logs:(id<FBDataConsumer>)consumer { return [self.logger tailToConsumer:consumer]; } - (FBFuture<NSDictionary<NSString *, id> *> *)diagnostic_information { id<FBDiagnosticInformationCommands> commands = (id<FBDiagnosticInformationCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBDiagnosticInformationCommands)]) { // Don't fail, just return empty. return [FBFuture futureWithResult:@{}]; } return [commands fetchDiagnosticInformation]; } - (FBFuture<NSNull *> *)hid:(FBSimulatorHIDEvent *)event { return [self.connectToHID onQueue:self.target.workQueue fmap:^FBFuture *(FBSimulatorHID *hid) { return [event performOnHID:hid]; }]; } - (FBFuture<NSNull *> *)set_hardware_keyboard_enabled:(BOOL)enabled { return [[self settingsCommands] onQueue:self.target.workQueue fmap:^(id<FBSimulatorSettingsCommands> commands) { return [commands setHardwareKeyboardEnabled:enabled]; }]; } - (FBFuture<NSNull *> *)set_preference:(NSString *)name value:(NSString *)value type:(NSString *)type domain:(nullable NSString *)domain { return [[self settingsCommands] onQueue:self.target.workQueue fmap:^(id<FBSimulatorSettingsCommands> commands) { return [commands setPreference:name value:value type:type domain:domain]; }]; } - (FBFuture<NSString *> *)get_preference:(NSString *)name domain:(nullable NSString *)domain { return [[self settingsCommands] onQueue:self.target.workQueue fmap:^(id<FBSimulatorSettingsCommands> commands) { return [commands getCurrentPreference:name domain:domain]; }]; } - (FBFuture<NSNull *> *)set_locale_with_identifier:(NSString *)identifier { return [[self settingsCommands] onQueue:self.target.workQueue fmap:^(id<FBSimulatorSettingsCommands> commands) { return [commands setPreference:@"AppleLocale" value:identifier type:nil domain:nil]; }]; } - (FBFuture<NSString *> *)get_current_locale_identifier { return [[self settingsCommands] onQueue:self.target.workQueue fmap:^(id<FBSimulatorSettingsCommands> commands) { return [commands getCurrentPreference:@"AppleLocale" domain:nil]; }]; } - (NSArray<NSString *> *)list_locale_identifiers { return NSLocale.availableLocaleIdentifiers; } #pragma mark File Commands - (FBFuture<NSNull *> *)move_paths:(NSArray<NSString *> *)originPaths to_path:(NSString *)destinationPath containerType:(NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^(id<FBFileContainer> container) { NSMutableArray<FBFuture<NSNull *> *> *futures = NSMutableArray.array; for (NSString *originPath in originPaths) { [futures addObject:[container moveFrom:originPath to:destinationPath]]; } return [[FBFuture futureWithFutures:futures] mapReplace:NSNull.null]; }]; } - (FBFuture<NSNull *> *)push_file_from_tar:(NSData *)tarData to_path:(NSString *)destinationPath containerType:(NSString *)containerType { return [[self.temporaryDirectory withArchiveExtracted:tarData] onQueue:self.target.workQueue pop:^FBFuture *(NSURL *extractionDirectory) { NSError *error; NSArray<NSURL *> *paths = [NSFileManager.defaultManager contentsOfDirectoryAtURL:extractionDirectory includingPropertiesForKeys:@[NSURLIsDirectoryKey] options:0 error:&error]; if (!paths) { return [FBFuture futureWithError:error]; } return [self push_files:paths to_path:destinationPath containerType:containerType]; }]; } - (FBFuture<NSNull *> *)push_files:(NSArray<NSURL *> *)paths to_path:(NSString *)destinationPath containerType:(NSString *)containerType { return [FBFuture onQueue:self.target.asyncQueue resolve:^FBFuture<NSNull *> *{ return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^FBFuture *(id<FBFileContainer> container) { NSMutableArray<FBFuture<NSNull *> *> *futures = NSMutableArray.array; for (NSURL *originPath in paths) { [futures addObject:[container copyFromHost:originPath.path toContainer:destinationPath]]; } return [[FBFuture futureWithFutures:futures] mapReplace:NSNull.null]; }]; }]; } - (FBFuture<NSString *> *)pull_file_path:(NSString *)path destination_path:(NSString *)destinationPath containerType:(NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^FBFuture *(id<FBFileContainer> commands) { return [commands copyFromContainer:path toHost:destinationPath]; }]; } - (FBFuture<NSData *> *)pull_file:(NSString *)path containerType:(NSString *)containerType { __block NSString *tempPath; return [[[self.temporaryDirectory withTemporaryDirectory] onQueue:self.target.workQueue pend:^(NSURL *url) { tempPath = [url.path stringByAppendingPathComponent:path.lastPathComponent]; return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^(id<FBFileContainer> container) { return [container copyFromContainer:path toHost:tempPath]; }]; }] onQueue:self.target.workQueue pop:^(id _) { return [FBArchiveOperations createGzippedTarDataForPath:tempPath queue:self.target.workQueue logger:self.target.logger]; }]; } - (FBFuture<FBFuture<NSNull *> *> *)tail:(NSString *)path to_consumer:(id<FBDataConsumer>)consumer in_container:(nullable NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^(id<FBFileContainer> container) { return [container tail:path toConsumer:consumer]; }]; } - (FBFuture<NSNull *> *)create_directory:(NSString *)directoryPath containerType:(NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^(id<FBFileContainer> targetApplicationData) { return [targetApplicationData createDirectory:directoryPath]; }]; } - (FBFuture<NSNull *> *)remove_paths:(NSArray<NSString *> *)paths containerType:(NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^FBFuture *(id<FBFileContainer> container) { NSMutableArray<FBFuture<NSNull *> *> *futures = NSMutableArray.array; for (NSString *path in paths) { [futures addObject:[container remove:path]]; } return [[FBFuture futureWithFutures:futures] mapReplace:NSNull.null]; }]; } - (FBFuture<NSArray<NSString *> *> *)list_path:(NSString *)path containerType:(NSString *)containerType { return [[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^FBFuture *(id<FBFileContainer> container) { return [container contentsOfDirectory:path]; }]; } - (FBFuture<NSDictionary<NSString *, NSArray<NSString *> *> *> *)list_paths:(NSArray<NSString *> *)paths containerType:(NSString *)containerType { return [[[self applicationDataContainerCommands:containerType] onQueue:self.target.workQueue pop:^FBFuture *(id<FBFileContainer> container) { NSMutableArray<FBFuture<NSArray<NSString *> *> *> *futures = NSMutableArray.array; for (NSString *path in paths) { [futures addObject:[container contentsOfDirectory:path]]; } return [FBFuture futureWithFutures:futures]; }] onQueue:self.target.asyncQueue map:^ (NSArray<NSArray<NSString *> *> *listings) { // Dictionary is constructed by attaching paths for ordering within array. return [NSDictionary dictionaryWithObjects:listings forKeys:paths]; }]; } - (FBFuture<FBProcess<id, id<FBDataConsumer>, NSString *> *> *) dapServerWithPath:(NSString *)dapPath stdIn:(FBProcessInput *)stdIn stdOut:(id<FBDataConsumer>)stdOut { id<FBDapServerCommand> commands = (id<FBDapServerCommand>) self.target; if (![commands conformsToProtocol:@protocol(FBDapServerCommand)]) { return [[FBControlCoreError describeFormat:@"Target doesn't conform to FBDapServerCommand protocol %@", commands] failFuture]; } return [commands launchDapServer:dapPath stdIn:stdIn stdOut:stdOut]; } #pragma mark Private Methods - (FBFutureContext<id<FBFileContainer>> *)applicationDataContainerCommands:(NSString *)containerType { if ([containerType isEqualToString:FBFileContainerKindCrashes]) { return [self.target crashLogFiles]; } id<FBFileCommands> commands = (id<FBFileCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBFileCommands)]) { return [[FBControlCoreError describeFormat:@"Target doesn't conform to FBFileCommands protocol %@", commands] failFutureContext]; } if ([containerType isEqualToString:FBFileContainerKindApplication]) { return [commands fileCommandsForApplicationContainers]; } if ([containerType isEqualToString:FBFileContainerKindGroup]) { return [commands fileCommandsForGroupContainers]; } if ([containerType isEqualToString:FBFileContainerKindMedia]) { return [commands fileCommandsForMediaDirectory]; } if ([containerType isEqualToString:FBFileContainerKindRoot]) { return [commands fileCommandsForRootFilesystem]; } if ([containerType isEqualToString:FBFileContainerKindProvisioningProfiles]) { return [commands fileCommandsForProvisioningProfiles]; } if ([containerType isEqualToString:FBFileContainerKindMDMProfiles]) { return [commands fileCommandsForMDMProfiles]; } if ([containerType isEqualToString:FBFileContainerKindSpringboardIcons]) { return [commands fileCommandsForSpringboardIconLayout]; } if ([containerType isEqualToString:FBFileContainerKindWallpaper]) { return [commands fileCommandsForWallpaper]; } if ([containerType isEqualToString:FBFileContainerKindDiskImages]) { return [commands fileCommandsForDiskImages]; } if ([containerType isEqualToString:FBFileContainerKindSymbols]) { return [commands fileCommandsForSymbols]; } if ([containerType isEqualToString:FBFileContainerKindAuxillary]) { return [commands fileCommandsForAuxillary]; } if ([containerType isEqualToString:FBFileContainerKindXctest]) { return [FBFutureContext futureContextWithResult:self.storageManager.xctest.asFileContainer]; } if ([containerType isEqualToString:FBFileContainerKindDylib]) { return [FBFutureContext futureContextWithResult:self.storageManager.dylib.asFileContainer]; } if ([containerType isEqualToString:FBFileContainerKindDsym]) { return [FBFutureContext futureContextWithResult:self.storageManager.dsym.asFileContainer]; } if ([containerType isEqualToString:FBFileContainerKindFramework]) { return [FBFutureContext futureContextWithResult:self.storageManager.framework.asFileContainer]; } if (containerType == nil || containerType.length == 0) { // The Default for no, or null container for back-compat. return [self.target isKindOfClass:FBDevice.class] ? [commands fileCommandsForMediaDirectory] : [commands fileCommandsForRootFilesystem]; } return [commands fileCommandsForContainerApplication:containerType]; } - (FBFuture<id<FBScreenshotCommands>> *)screenshotCommands { id<FBScreenshotCommands> commands = (id<FBScreenshotCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBScreenshotCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBScreenshotCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<id<FBSimulatorLifecycleCommands>> *)lifecycleCommands { id<FBSimulatorLifecycleCommands> commands = (id<FBSimulatorLifecycleCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBSimulatorLifecycleCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBSimulatorLifecycleCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<id<FBSimulatorMediaCommands>> *)mediaCommands { id<FBSimulatorMediaCommands> commands = (id<FBSimulatorMediaCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBSimulatorMediaCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBSimulatorMediaCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<id<FBSimulatorKeychainCommands>> *)keychainCommands { id<FBSimulatorKeychainCommands> commands = (id<FBSimulatorKeychainCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBSimulatorKeychainCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBSimulatorKeychainCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<id<FBSimulatorSettingsCommands>> *)settingsCommands { id<FBSimulatorSettingsCommands> commands = (id<FBSimulatorSettingsCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBSimulatorSettingsCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBSimulatorSettingsCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<id<FBAccessibilityCommands>> *)accessibilityCommands { id<FBAccessibilityCommands> commands = (id<FBAccessibilityCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBAccessibilityCommands)]) { return [[FBIDBError describeFormat:@"Target doesn't conform to FBAccessibilityCommands protocol %@", self.target] failFuture]; } return [FBFuture futureWithResult:commands]; } - (FBFuture<FBSimulatorHID *> *)connectToHID { return [[self lifecycleCommands] onQueue:self.target.workQueue fmap:^ FBFuture<FBSimulatorHID *> * (id<FBSimulatorLifecycleCommands> commands) { NSError *error = nil; if (![FBSimulatorControlFrameworkLoader.xcodeFrameworks loadPrivateFrameworks:self.target.logger error:&error]) { return [[FBIDBError describeFormat:@"SimulatorKit is required for HID interactions: %@", error] failFuture]; } return [commands connectToHID]; }]; } - (FBFuture<FBInstalledArtifact *> *)installExtractedApp:(FBFutureContext<NSURL *> *)extractedAppContext makeDebuggable:(BOOL)makeDebuggable { FBFutureContext<FBBundleDescriptor *> *bundleContext = [extractedAppContext onQueue:self.target.asyncQueue pend:^(NSURL *extractPath) { return [FBBundleDescriptor findAppPathFromDirectory:extractPath]; }]; return [self installAppBundle:bundleContext makeDebuggable:makeDebuggable]; } - (FBFuture<FBInstalledArtifact *> *)installAppBundle:(FBFutureContext<FBBundleDescriptor *> *)bundleContext makeDebuggable:(BOOL)makeDebuggable { BOOL userDevelopmentAppIsRequired = [self.target isKindOfClass:FBDevice.class]; return [bundleContext onQueue:self.target.asyncQueue pop:^(FBBundleDescriptor *appBundle){ if (!appBundle) { return [FBFuture futureWithError:[FBControlCoreError errorForDescription:@"No app bundle could be extracted"]]; } NSError *error = nil; if (![self.storageManager.application checkArchitecture:appBundle error:&error]) { return [FBFuture futureWithError:error]; } return [[FBFuture futureWithFutures:@[ [self.target installApplicationWithPath:appBundle.path], // TODO: currently we have to persist it even if app is not used for debugging // as installed apps are referenced from xctestrun files and expanded by idb // by using its own application storage. Fix this by replacing xctestrun // placeholders by app bundle paths instead [self.storageManager.application saveBundle:appBundle] ]] onQueue:self.target.asyncQueue fmap:^(NSArray<id> *tuple) { FBInstalledApplication *installedApp = tuple[0]; if (makeDebuggable && installedApp.installType != FBApplicationInstallTypeUserDevelopment && userDevelopmentAppIsRequired) { return [[FBIDBError describeFormat:@"Requested debuggable install of %@ but User Development signing is required", installedApp] failFuture]; } return [FBFuture futureWithResult:[[FBInstalledArtifact alloc] initWithName:appBundle.identifier uuid:appBundle.binary.uuid path:[NSURL fileURLWithPath:installedApp.bundle.path]]]; }]; }]; } - (FBFuture<FBInstalledArtifact *> *)installXctest:(FBFutureContext<NSURL *> *)extractedXctest { return [extractedXctest onQueue:self.target.workQueue pop:^(NSURL *extractionDirectory) { return [self.storageManager.xctest saveBundleOrTestRunFromBaseDirectory:extractionDirectory]; }]; } - (FBFuture<FBInstalledArtifact *> *)installXctestFilePath:(FBFutureContext<NSURL *> *)bundle { return [bundle onQueue:self.target.workQueue pop:^(NSURL *xctestURL) { return [self.storageManager.xctest saveBundleOrTestRun:xctestURL]; }]; } - (FBFuture<FBInstalledArtifact *> *)installFile:(FBFutureContext<NSURL *> *)extractedFileContext intoStorage:(FBFileStorage *)storage { return [extractedFileContext onQueue:self.target.workQueue pop:^(NSURL *extractedFile) { NSError *error = nil; FBInstalledArtifact *artifact = [storage saveFile:extractedFile error:&error]; if (!artifact) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:artifact]; }]; } // To navigate directly to the dSYM directory instead of the parent tmp directory // created while unzipping - (FBFutureContext<NSURL *> *)dsymDirnameFromUnzipDir:(FBFutureContext<NSURL *> *)extractedFileContext { return [extractedFileContext onQueue:self.target.workQueue pend:^(NSURL *parentDir) { NSError *error = nil; NSArray<NSURL *> *subDirs = [NSFileManager.defaultManager contentsOfDirectoryAtURL:parentDir includingPropertiesForKeys:@[NSURLIsDirectoryKey] options:0 error:&error]; if (!subDirs) { return [FBFuture futureWithError:error]; } if ([subDirs count] != 1) { // if more than one dSYM is found // then we treat the parent dir as the dSYM directory return [FBFuture futureWithResult:parentDir]; } return [FBFuture futureWithResult:subDirs[0]]; }]; } // Will install the dsym under standard dsym location // if linkTo is passed: // after installation it will create a symlink in the bundle container - (FBFuture<FBInstalledArtifact *> *)installAndLinkDsym:(FBFutureContext<NSURL *> *)extractedFileContext intoStorage:(FBFileStorage *)storage linkTo:(nullable FBDsymInstallLinkToBundle *)linkTo { return [extractedFileContext onQueue:self.target.workQueue pop:^(NSURL *extractionDir) { NSError *error = nil; FBInstalledArtifact *artifact = [storage saveFileInUniquePath:extractionDir error:&error]; if (!artifact) { return [FBFuture futureWithError:error]; } if (!linkTo) { return [FBFuture futureWithResult:artifact]; } FBFuture<NSURL *> *future = nil; if (linkTo.bundle_type == FBDsymBundleTypeApp) { future = [[self.target installedApplicationWithBundleID:linkTo.bundle_id] onQueue:self.target.workQueue fmap:^(FBInstalledApplication *linkToApp) { [self.logger logFormat:@"Going to create a symlink for app bundle: %@", linkToApp.bundle.name]; return [FBFuture futureWithResult:[NSURL fileURLWithPath:linkToApp.bundle.path]]; }]; } else { id<FBXCTestDescriptor> testDescriptor = [self.storageManager.xctest testDescriptorWithID:linkTo.bundle_id error:&error]; [self.logger logFormat:@"Going to create a symlink for test bundle: %@", testDescriptor.name]; future = [FBFuture futureWithResult:testDescriptor.url]; } return [future onQueue:self.target.workQueue fmap:^(NSURL *bundlePath) { NSURL *bundleUrl = [bundlePath URLByDeletingLastPathComponent]; NSURL *dsymURL = [bundleUrl URLByAppendingPathComponent:artifact.path.lastPathComponent]; // delete a simlink if already exists // TODO: check if what we are deleting is a symlink [NSFileManager.defaultManager removeItemAtURL:dsymURL error:nil]; [self.logger logFormat:@"Deleted a symlink for dsym if it already exists: %@", dsymURL]; NSError *createLinkError = nil; if (![NSFileManager.defaultManager createSymbolicLinkAtURL:dsymURL withDestinationURL:artifact.path error:&createLinkError]){ return [FBFuture futureWithError:error]; } [self.logger logFormat:@"Created a symlink for dsym from: %@ to %@", dsymURL, artifact.path]; return [FBFuture futureWithResult:artifact]; }]; }]; } - (FBFuture<FBInstalledArtifact *> *)installBundle:(FBFutureContext<NSURL *> *)extractedDirectoryContext intoStorage:(FBBundleStorage *)storage { return [extractedDirectoryContext onQueue:self.target.workQueue pop:^ FBFuture<FBInstalledArtifact *> * (NSURL *extractedDirectory) { NSError *error = nil; FBBundleDescriptor *bundle = [FBStorageUtils bundleInDirectory:extractedDirectory error:&error]; if (!bundle) { return [FBFuture futureWithError:error]; } return [storage saveBundle:bundle]; }]; } - (FBFuture<NSNull *> *)sendPushNotificationForBundleID:(NSString *)bundleID jsonPayload:(NSString *)jsonPayload { id<FBNotificationCommands> commands = (id<FBNotificationCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBNotificationCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBNotificationCommands", commands] failFuture]; } return [commands sendPushNotificationForBundleID:bundleID jsonPayload:jsonPayload]; } - (FBFuture<NSNull *> *)simulateMemoryWarning { id<FBMemoryCommands> commands = (id<FBMemoryCommands>) self.target; if (![commands conformsToProtocol:@protocol(FBMemoryCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBMemoryCommands", commands] failFuture]; } return [commands simulateMemoryWarning]; } @end