idb_companion/main.m (632 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 <Foundation/Foundation.h> #import <FBControlCore/FBControlCore.h> #import <FBDeviceControl/FBDeviceControl.h> #import <FBSimulatorControl/FBSimulatorControl.h> #import <XCTestBootstrap/XCTestBootstrap.h> #import "FBIDBCompanionServer.h" #import "FBIDBConfiguration.h" #import "FBIDBError.h" #import "FBIDBLogger.h" #import "FBIDBPortsConfiguration.h" #import "FBiOSTargetProvider.h" #import "FBiOSTargetStateChangeNotifier.h" #import "FBiOSTargetDescription.h" #import "FBIDBStorageManager.h" #import "FBIDBCommandExecutor.h" #import "idb-Swift.h" const char *kUsageHelpMessage = "\ Usage: \n \ Modes of operation, only one of these may be specified:\n\ --udid UDID|mac|only Launches a companion server for the specified UDID, 'mac' for a mac companion, or 'only' to run a companion for the only simulator/device available.\n\ --boot UDID Boots the simulator with the specified UDID.\n\ --reboot UDID Reboots the target with the specified UDID.\n\ --shutdown UDID Shuts down the target with the specified UDID.\n\ --erase UDID Erases the target with the specified UDID.\n\ --clean UDID Performs a soft reset to the specified UDID.\n\ --delete UDID|all Deletes the simulator with the specified UDID, or 'all' to delete all simulators in the set.\n\ --create VALUE Creates a simulator using the VALUE argument like \"iPhone X,iOS 12.4\"\n\ --clone UDID Clones a simulator by a given UDID\n\ --clone-destination-set A path to the destination device set in a clone operation, --device-set-path specifies the source simulator.\n\ --recover ecid:ECID Causes the targeted device ECID to enter recovery mode\n\ --unrecover ecid:ECID Causes the targeted device ECID to exit recovery mode\n\ --activate ecid:ECID Causes the device to activate\n\ --notify PATH|stdout Launches a companion notifier which will stream availability updates to the specified path, or stdout.\n\ --forward UDID:PORT Forwards the remote socket for the specified UDID to the specified remote PORT. Input and output is relayed via stdin/stdout\n\ --list 1 Lists all available devices and simulators in the current context. If Xcode is not correctly installed, only devices will be listed.\n\ --version Writes companion version information to stdout.\n\ --help Show this help message and exit.\n\ \n\ Options:\n\ --grpc-port PORT Port to start the grpc companion server on (default: 10882).\n\ --tls-cert-path PATH If specified exposed GRPC server will be listening on a TLS enabled socket.\n\ --grpc-domain-sock PATH Unix Domain Socket path to start the companion server on, will superceed TCP binding via --grpc-port.\n\ --debug-port PORT Port to connect debugger on (default: 10881).\n\ --log-file-path PATH Path to write a log file to e.g ./output.log (default: logs to stdErr).\n\ --log-level info|debug The log level to use, 'debug' for a higher level of debugging 'info' for a lower level of logging (default 'debug').\n\ --device-set-path PATH Path to a custom Simulator device set.\n\ --only FILTER_OPTION If provided, will limit interaction to a subset of all available targets\n\ --headless VALUE If VALUE is a true value, the Simulator boot's lifecycle will be tied to the lifecycle of this invocation.\n\ --verify-booted VALUE If VALUE is a true value, will verify that the Simulator is in a known-booted state before --boot completes. Default is true.\n\ --terminate-offline VALUE Terminate if the target goes offline, otherwise the companion will stay alive.\n\ \n\ Filter Options:\n\ simulator Limit interactions to Simulators only.\n\ device Limit interactions to Devices only.\n\ ecid:ECID Limit interactions to a specific Device ECID\n\ "; static void WriteJSONToStdOut(id json) { NSData *jsonOutput = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil]; NSMutableData *readyOutput = [NSMutableData dataWithData:jsonOutput]; [readyOutput appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; write(STDOUT_FILENO, readyOutput.bytes, readyOutput.length); fflush(stdout); } static void WriteTargetToStdOut(id<FBiOSTargetInfo> target) { WriteJSONToStdOut([[FBiOSTargetDescription alloc] initWithTarget:target].asJSON); } static FBFuture<FBSimulatorSet *> *SimulatorSetWithPath(NSString *deviceSetPath, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { // Give a more meaningful message if we can't load the frameworks. NSError *error = nil; if(![FBSimulatorControlFrameworkLoader.essentialFrameworks loadPrivateFrameworks:logger error:&error]) { return [FBFuture futureWithError:error]; } FBSimulatorControlConfiguration *configuration = [FBSimulatorControlConfiguration configurationWithDeviceSetPath:deviceSetPath logger:logger reporter:reporter]; FBSimulatorControl *control = [FBSimulatorControl withConfiguration:configuration error:&error]; if (!control) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:control.set]; } static FBFuture<FBSimulatorSet *> *SimulatorSet(NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { NSString *deviceSetPath = [userDefaults stringForKey:@"-device-set-path"]; return SimulatorSetWithPath(deviceSetPath, logger, reporter); } static FBFuture<FBDeviceSet *> *DeviceSet(id<FBControlCoreLogger> logger, NSString *ecidFilter) { return [[FBFuture onQueue:dispatch_get_main_queue() resolveValue:^ FBDeviceSet * (NSError **error) { // Give a more meaningful message if we can't load the frameworks. if(![FBDeviceControlFrameworkLoader.new loadPrivateFrameworks:logger error:error]) { return nil; } FBDeviceSet *deviceSet = [FBDeviceSet setWithLogger:logger delegate:nil ecidFilter:ecidFilter error:error]; if (!deviceSet) { return nil; } return deviceSet; }] delay:0.2]; // This is needed to give the Restorable Devices time to populate. } static FBFuture<NSArray<id<FBiOSTargetSet>> *> *DefaultTargetSets(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { NSString *only = [userDefaults stringForKey:@"-only"]; if (only) { if ([only.lowercaseString containsString:@"simulator"]) { [logger log:@"'--only' set for Simulators"]; return [FBFuture futureWithFutures:@[SimulatorSet(userDefaults, logger, reporter)]]; } if ([only.lowercaseString containsString:@"device"]) { [logger log:@"'--only' set for Devices"]; return [FBFuture futureWithFutures:@[DeviceSet(logger, nil)]]; } if ([only.lowercaseString hasPrefix:@"ecid:"]) { NSString *ecid = [only.lowercaseString stringByReplacingOccurrencesOfString:@"ecid:" withString:@""]; [logger logFormat:@"ECID filter of %@", ecid]; return [FBFuture futureWithFutures:@[DeviceSet(logger, ecid)]]; } return [[FBIDBError describeFormat:@"%@ is not a valid argument for '--only'", only] failFuture]; } if (!xcodeAvailable) { [logger log:@"Xcode is not available, only Devices will be provided"]; return [FBFuture futureWithFutures:@[DeviceSet(logger, nil)]]; } [logger log:@"Providing targets across Simulator and Device sets."]; return [FBFuture futureWithFutures:@[ SimulatorSet(userDefaults, logger, reporter), DeviceSet(logger, nil), ]]; } static FBFuture<id<FBiOSTarget>> *TargetForUDID(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, BOOL warmUp, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^(NSArray<id<FBiOSTargetSet>> *targetSets) { return [FBiOSTargetProvider targetWithUDID:udid targetSets:targetSets warmUp:warmUp logger:logger]; }]; } static FBFuture<FBDevice *> *DeviceForECID(NSString *ecid, id<FBControlCoreLogger> logger) { return [DeviceSet(logger, [ecid stringByReplacingOccurrencesOfString:@"ecid:" withString:@""]) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDeviceSet *deviceSet) { NSArray<FBDevice *> *devices = deviceSet.allDevices; if (devices.count == 0) { return [[FBIDBError describeFormat:@"No devices %@ matching %@", [FBCollectionInformation oneLineDescriptionFromArray:devices], ecid] failFuture]; } return [FBFuture futureWithResult:devices.firstObject]; }]; } static FBFuture<FBSimulator *> *SimulatorFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [[SimulatorSet(userDefaults, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^(FBSimulatorSet *simulatorSet) { return [FBiOSTargetProvider targetWithUDID:udid targetSets:@[simulatorSet] warmUp:NO logger:logger]; }] onQueue:dispatch_get_main_queue() fmap:^(id<FBiOSTarget> target) { id<FBSimulatorLifecycleCommands> commands = (id<FBSimulatorLifecycleCommands>) target; if (![commands conformsToProtocol:@protocol(FBSimulatorLifecycleCommands)]) { return [[FBIDBError describeFormat:@"%@ does not support Simulator Lifecycle commands", commands] failFuture]; } return [FBFuture futureWithResult:commands]; }]; } static FBFuture<NSNull *> *TargetOfflineFuture(id<FBiOSTarget> target, id<FBControlCoreLogger> logger) { return [[target resolveLeavesState:FBiOSTargetStateBooted] onQueue:target.workQueue doOnResolved:^(id _){ [target.logger log:@"Target is no longer booted, companion going offline"]; }]; } static FBFuture<FBFuture<NSNull *> *> *BootFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { BOOL headless = [userDefaults boolForKey:@"-headless"]; BOOL verifyBooted = [userDefaults objectForKey:@"-verify-booted"] == nil ? YES : [userDefaults boolForKey:@"-verify-booted"]; return [[SimulatorFuture(udid, userDefaults, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^(FBSimulator *simulator) { // Boot the simulator with the options provided. FBSimulatorBootOptions options = FBSimulatorBootConfiguration.defaultConfiguration.options; if (headless) { [logger logFormat:@"Booting %@ headlessly", udid]; options = options | FBSimulatorBootOptionsTieToProcessLifecycle; } else { [logger logFormat:@"Booting %@ normally", udid]; options = options & ~FBSimulatorBootOptionsTieToProcessLifecycle; } if (verifyBooted) { [logger logFormat:@"Booting %@ with verification", udid]; options = options | FBSimulatorBootOptionsVerifyUsable; } else { [logger logFormat:@"Booting %@ without verification", udid]; options = options & ~FBSimulatorBootOptionsVerifyUsable; } FBSimulatorBootConfiguration *config = [[FBSimulatorBootConfiguration alloc] initWithOptions:options environment:@{}]; return [[simulator boot:config] mapReplace:simulator]; }] onQueue:dispatch_get_main_queue() map:^ FBFuture<NSNull *> * (FBSimulator *simulator) { // Write the boot success to stdout WriteTargetToStdOut(simulator); // In a headless boot: // - We need to keep this process running until it's otherwise shutdown. When the sim is shutdown this process will die. // - If this process is manually killed then the simulator will die // For a regular boot the sim will outlive this process. if (!headless) { return FBFuture.empty; } // Whilst we can rely on this process being killed shutting the simulator, this is asynchronous. // This means that we should attempt to handle cancellation gracefully. // In this case we should attempt to shutdown in response to cancellation. // This means if this future is cancelled and waited-for before the process exits we will return it in a "Shutdown" state. return [TargetOfflineFuture(simulator, logger) onQueue:dispatch_get_main_queue() respondToCancellation:^{ return [simulator shutdown]; }]; }]; } static FBFuture<NSNull *> *ShutdownFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) { id<FBPowerCommands> commands = (id<FBPowerCommands>) target; if (![commands conformsToProtocol:@protocol(FBPowerCommands)]) { return [[FBIDBError describeFormat:@"Cannot shutdown %@, does not support shutting down", target] failFuture]; } return [commands shutdown]; }]; } static FBFuture<NSNull *> *RebootFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) { id<FBPowerCommands> commands = (id<FBPowerCommands>) target; if (![commands conformsToProtocol:@protocol(FBPowerCommands)]) { return [[FBIDBError describeFormat:@"Cannot shutdown %@, does not support rebooting", target] failFuture]; } return [commands reboot]; }]; } static FBFuture<NSNull *> *EraseFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) { id<FBEraseCommands> commands = (id<FBEraseCommands>) target; if (![commands conformsToProtocol:@protocol(FBEraseCommands)]) { return [[FBIDBError describeFormat:@"Cannot erase %@, does not support erasing", target] failFuture]; } return [commands erase]; }]; } static FBFuture<NSNull *> *DeleteFuture(NSString *udidOrAll, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [[SimulatorSet(userDefaults, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture * (FBSimulatorSet *set) { if ([udidOrAll.lowercaseString isEqualToString:@"all"]) { return [set deleteAll]; } FBSimulator *simulator = [set simulatorWithUDID:udidOrAll]; if (!simulator) { return [[FBIDBError describeFormat:@"Could not find a simulator with udid %@", udidOrAll] failFuture]; } return [set delete:simulator]; }] mapReplace:NSNull.null]; } static FBFuture<NSNull *> *ListFuture(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter) onQueue:dispatch_get_main_queue() map:^ NSNull * (NSArray<id<FBiOSTargetSet>> *targetSets) { NSUInteger reportedCount = 0; for (id<FBiOSTargetSet> targetSet in targetSets) { for (id<FBiOSTargetInfo> targetInfo in targetSet.allTargetInfos) { WriteTargetToStdOut(targetInfo); reportedCount++; } } [logger logFormat:@"Reported %lu targets to stdout", reportedCount]; return NSNull.null; }]; } static FBFuture<NSNull *> *CreateFuture(NSString *create, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [[SimulatorSet(userDefaults, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<FBSimulator *> * (FBSimulatorSet *set) { NSArray<NSString *> *parameters = [create componentsSeparatedByString:@","]; FBSimulatorConfiguration *config = [FBSimulatorConfiguration defaultConfiguration]; if (parameters.count > 0) { config = [config withDeviceModel:parameters[0]]; } if (parameters.count > 1) { config = [config withOSNamed:parameters[1]]; } return [set createSimulatorWithConfiguration:config]; }] onQueue:dispatch_get_main_queue() map:^(FBSimulator *simulator) { WriteTargetToStdOut(simulator); return NSNull.null; }]; } static FBFuture<NSNull *> *CloneFuture(NSString *udid, NSUserDefaults *userDefaults, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { NSString *destinationSet = [userDefaults stringForKey:@"-clone-destination-set"]; return [[[FBFuture futureWithFutures:@[ SimulatorFuture(udid, userDefaults, logger, reporter), SimulatorSetWithPath(destinationSet, logger, reporter), ]] onQueue:dispatch_get_main_queue() fmap:^ FBFuture<FBSimulator *> * (NSArray<id> *tuple) { FBSimulator *base = tuple[0]; FBSimulatorSet *destination = tuple[1]; return [base.set cloneSimulator:base toDeviceSet:destination]; }] onQueue:dispatch_get_main_queue() map:^(FBSimulator *cloned) { WriteTargetToStdOut(cloned); return NSNull.null; }]; } static FBFuture<NSNull *> *EnterRecoveryFuture(NSString *ecid, id<FBControlCoreLogger> logger) { return [DeviceForECID(ecid, logger) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) { return [device enterRecovery]; }]; } static FBFuture<NSNull *> *ExitRecoveryFuture(NSString *ecid, id<FBControlCoreLogger> logger) { return [DeviceForECID(ecid, logger) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) { return [device exitRecovery]; }]; } static FBFuture<NSNull *> *ActivateFuture(NSString *ecid, id<FBControlCoreLogger> logger) { return [DeviceForECID(ecid, logger) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (FBDevice *device) { return [device activate]; }]; } static FBFuture<NSNull *> *CleanFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [TargetForUDID(udid, userDefaults, xcodeAvailable, YES, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^FBFuture<NSNull *> *(id<FBiOSTarget> target) { NSError *error = nil; FBIDBStorageManager *storageManager = [FBIDBStorageManager managerForTarget:target logger:logger error:&error]; if (!storageManager) { return [FBFuture futureWithError:error]; } FBIDBCommandExecutor *commandExecutor = [FBIDBCommandExecutor commandExecutorForTarget:target storageManager:storageManager temporaryDirectory:[FBTemporaryDirectory temporaryDirectoryWithLogger:logger] ports:[FBIDBPortsConfiguration portsWithArguments:userDefaults] logger:logger]; return [commandExecutor clean]; }]; } static FBFuture<FBFuture<NSNull *> *> *CompanionServerFuture(NSString *udid, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { BOOL terminateOffline = [userDefaults boolForKey:@"-terminate-offline"]; return [TargetForUDID(udid, userDefaults, xcodeAvailable, YES, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^(id<FBiOSTarget> target) { [reporter addMetadata:@{ @"udid": udid, @"target_type": FBiOSTargetTypeStringFromTargetType(target.targetType).lowercaseString, }]; [reporter report:[FBEventReporterSubject subjectForEvent:@"launched"]]; // Start up the companion FBIDBPortsConfiguration *ports = [FBIDBPortsConfiguration portsWithArguments:userDefaults]; BOOL withSwiftServer = ports.grpcSwiftPort != 0; FBTemporaryDirectory *temporaryDirectory = [FBTemporaryDirectory temporaryDirectoryWithLogger:logger]; NSError *error = nil; FBIDBStorageManager *storageManager = [FBIDBStorageManager managerForTarget:target logger:logger error:&error]; if (!storageManager) { return [FBFuture futureWithError:error]; } // Command Executor FBIDBCommandExecutor *commandExecutor = [FBIDBCommandExecutor commandExecutorForTarget:target storageManager:storageManager temporaryDirectory:temporaryDirectory ports:ports logger:logger]; FBIDBCommandExecutor *cppCommandExecutor = [FBLoggingWrapper wrap:commandExecutor simplifiedNaming:YES eventReporter:reporter logger:logger]; FBIDBCommandExecutor *swiftCommandExecutor = [FBLoggingWrapper wrap:commandExecutor simplifiedNaming:YES eventReporter:FBIDBConfiguration.swiftEventReporter logger:logger]; FBIDBCompanionServer *server = [FBIDBCompanionServer companionForTarget:target commandExecutor: cppCommandExecutor ports:ports eventReporter:reporter logger:logger error:&error]; if (!server) { return [FBFuture futureWithError:error]; } GRPCSwiftServer *swiftServer = nil; if (withSwiftServer) { swiftServer = [[GRPCSwiftServer alloc] initWithTarget:target commandExecutor:swiftCommandExecutor reporter:reporter logger:logger ports:ports error:&error]; } if (error) { return [FBFuture futureWithError:error]; } return [[[server start] onQueue:target.workQueue fmap:^FBFuture * _Nonnull(NSDictionary<NSString *,id> * _Nonnull cppServerDescrption) { if (withSwiftServer) { return [[swiftServer start] onQueue:target.workQueue map: ^(NSDictionary<NSString *,id> * _Nonnull swiftServerDescription) { NSMutableDictionary<NSString *,id> * resultDictionary = [cppServerDescrption mutableCopy]; [resultDictionary addEntriesFromDictionary:swiftServerDescription]; return resultDictionary; }]; } return [FBFuture futureWithResult: cppServerDescrption]; }] onQueue:target.workQueue map:^ FBFuture * (NSDictionary<NSString *, id> *serverDescription) { WriteJSONToStdOut(serverDescription); NSMutableArray<FBFuture<NSNull *> *> *futures = [[NSMutableArray alloc] initWithArray: @[server.completed]]; if (swiftServer) { [futures addObject: swiftServer.completed]; } if (terminateOffline) { [logger.info logFormat:@"Companion will terminate when target goes offline"]; [futures addObject:TargetOfflineFuture(target, logger)]; } else { [logger.info logFormat:@"Companion will stay alive if target goes offline"]; } FBFuture<NSNull *> *completed = [FBFuture race: futures]; return [completed onQueue:target.workQueue chain:^(FBFuture *future) { [temporaryDirectory cleanOnExit]; return future; }]; }]; }]; } static FBFuture<FBFuture<NSNull *> *> *NotiferFuture(NSString *notify, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { return [[[DefaultTargetSets(userDefaults, xcodeAvailable, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^(NSArray<id<FBiOSTargetSet>> *targetSets) { if ([notify isEqualToString:@"stdout"]) { return [FBiOSTargetStateChangeNotifier notifierToStdOutWithTargetSets:targetSets logger:logger]; } return [FBiOSTargetStateChangeNotifier notifierToFilePath:notify withTargetSets:targetSets logger:logger]; }] onQueue:dispatch_get_main_queue() fmap:^(FBiOSTargetStateChangeNotifier *notifier) { [logger logFormat:@"Starting Notifier %@", notifier]; return [[notifier startNotifier] mapReplace:notifier]; }] onQueue:dispatch_get_main_queue() map:^(FBiOSTargetStateChangeNotifier *notifier) { [logger logFormat:@"Started Notifier %@", notifier]; return [notifier.notifierDone onQueue:dispatch_get_main_queue() respondToCancellation:^{ [logger logFormat:@"Stopping Notifier %@", notifier]; return FBFuture.empty; }]; }]; } static FBFuture<NSNull *> *ForwardFuture(NSString *forward, NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger, id<FBEventReporter> reporter) { NSArray<NSString *> *components = [forward componentsSeparatedByString:@":"]; if (components.count != 2) { return [[FBIDBError describeFormat:@"%@ should be of the form UDID:PORT", forward] failFuture]; } NSString *udid = components[0]; int remotePort = [components[1] intValue]; return [TargetForUDID(udid, userDefaults, xcodeAvailable, NO, logger, reporter) onQueue:dispatch_get_main_queue() fmap:^ FBFuture<NSNull *> * (id<FBiOSTarget> target) { id<FBSocketForwardingCommands> commands = (id<FBSocketForwardingCommands>) target; if (![commands conformsToProtocol:@protocol(FBSocketForwardingCommands)]) { return [[FBIDBError describeFormat:@"%@ does not conform to FBSocketForwardingCommands", target] failFuture]; } return [commands drainLocalFileInput:STDIN_FILENO localFileOutput:STDOUT_FILENO remotePort:remotePort]; }]; } static FBFuture<FBFuture<NSNull *> *> *GetCompanionCompletedFuture(NSUserDefaults *userDefaults, BOOL xcodeAvailable, id<FBControlCoreLogger> logger) { NSString *boot = [userDefaults stringForKey:@"-boot"]; NSString *reboot = [userDefaults stringForKey:@"-reboot"]; NSString *clone = [userDefaults stringForKey:@"-clone"]; NSString *create = [userDefaults stringForKey:@"-create"]; NSString *delete = [userDefaults stringForKey:@"-delete"]; NSString *erase = [userDefaults stringForKey:@"-erase"]; NSString *list = [userDefaults stringForKey:@"-list"]; NSString *notify = [userDefaults stringForKey:@"-notify"]; NSString *recover = [userDefaults stringForKey:@"-recover"]; NSString *shutdown = [userDefaults stringForKey:@"-shutdown"]; NSString *udid = [userDefaults stringForKey:@"-udid"]; NSString *unrecover = [userDefaults stringForKey:@"-unrecover"]; NSString *activate = [userDefaults stringForKey:@"-activate"]; NSString *clean = [userDefaults stringForKey:@"-clean"]; NSString *forward = [userDefaults stringForKey:@"-forward"]; id<FBEventReporter> reporter = FBIDBConfiguration.eventReporter; if (udid) { return CompanionServerFuture(udid, userDefaults, xcodeAvailable, logger, reporter); } else if (list) { [logger.info log:@"Listing"]; return [FBFuture futureWithResult:ListFuture(userDefaults, xcodeAvailable, logger, reporter)]; } else if (notify) { [logger.info logFormat:@"Notifying %@", notify]; return NotiferFuture(notify, userDefaults, xcodeAvailable, logger, reporter); } else if (boot) { [logger logFormat:@"Booting %@", boot]; return BootFuture(boot, userDefaults, logger, reporter); } else if (shutdown) { [logger.info logFormat:@"Shutting down %@", shutdown]; return [FBFuture futureWithResult:ShutdownFuture(shutdown, userDefaults, xcodeAvailable, logger, reporter)]; } else if (reboot) { [logger.info logFormat:@"Rebooting %@", reboot]; return [FBFuture futureWithResult:RebootFuture(reboot, userDefaults, xcodeAvailable, logger, reporter)]; } else if (erase) { [logger.info logFormat:@"Erasing %@", erase]; return [FBFuture futureWithResult:EraseFuture(erase, userDefaults, xcodeAvailable, logger, reporter)]; } else if (delete) { [logger.info logFormat:@"Deleting %@", delete]; return [FBFuture futureWithResult:DeleteFuture(delete, userDefaults, logger, reporter)]; } else if (create) { [logger.info logFormat:@"Creating %@", create]; return [FBFuture futureWithResult:CreateFuture(create, userDefaults, logger, reporter)]; } else if (clone) { [logger.info logFormat:@"Cloning %@", clone]; return [FBFuture futureWithResult:CloneFuture(clone, userDefaults, logger, reporter)]; } else if (recover) { [logger.info logFormat:@"Putting %@ into recovery", recover]; return [FBFuture futureWithResult:EnterRecoveryFuture(recover, logger)]; } else if (unrecover) { [logger.info logFormat:@"Removing %@ from recovery", recover]; return [FBFuture futureWithResult:ExitRecoveryFuture(unrecover, logger)]; } else if (activate) { [logger.info logFormat:@"Activating %@", activate]; return [FBFuture futureWithResult:ActivateFuture(activate, logger)]; } else if (clean) { [logger.info logFormat:@"Cleaning %@", clean]; return [FBFuture futureWithResult:CleanFuture(clean, userDefaults, xcodeAvailable, logger, reporter)]; } else if (forward) { [logger.info logFormat:@"Forwarding %@", forward]; return [FBFuture futureWithResult:ForwardFuture(forward, userDefaults, xcodeAvailable, logger, reporter)]; } return [[FBIDBError describeFormat:@"You must specify at least one 'Mode of operation'\n\n%s", kUsageHelpMessage] failFuture]; } static FBFuture<NSNumber *> *signalHandlerFuture(uintptr_t signalCode, NSString *exitMessage, id<FBControlCoreLogger> logger) { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); FBMutableFuture<NSNumber *> *future = FBMutableFuture.future; dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, signalCode, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(source, ^{ [logger.error log:exitMessage]; [future resolveWithResult:@(signalCode)]; }); dispatch_resume(source); struct sigaction action = {{0}}; action.sa_handler = SIG_IGN; sigaction((int)signalCode, &action, NULL); return [future onQueue:queue notifyOfCompletion:^(FBFuture *_) { dispatch_cancel(source); }]; } static NSString *EnvDescription() { return [FBCollectionInformation oneLineDescriptionFromDictionary:FBControlCoreGlobalConfiguration.safeSubprocessEnvironment]; } static NSString *ArchName() { #if TARGET_CPU_ARM64 return @"arm64"; #elif TARGET_CPU_X86_64 return @"x86_64"; #else return @"not supported"); #endif } static void logStartupInfo(FBIDBLogger *logger) { [logger.info logFormat:@"IDB Companion Built at %s %s", __DATE__, __TIME__]; [logger.info logFormat:@"IDB Companion architecture %@", ArchName()]; [logger.info logFormat:@"Invoked with args=%@ env=%@", [FBCollectionInformation oneLineDescriptionFromArray:NSProcessInfo.processInfo.arguments], EnvDescription()]; } int main(int argc, const char *argv[]) { @autoreleasepool { NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments; if ([arguments containsObject:@"--help"]) { fprintf(stderr, "%s", kUsageHelpMessage); return 1; } if ([arguments containsObject:@"--version"]) { WriteJSONToStdOut(@{@"build_time": @(__TIME__), @"build_date": @(__DATE__)}); return 0; } NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults; FBIDBLogger *logger = [FBIDBLogger loggerWithUserDefaults:userDefaults]; logStartupInfo(logger); NSError *error = nil; // Check that xcode-select returns a valid path, throw a big // warning if not BOOL xcodeAvailable = [FBXcodeDirectory.xcodeSelectDeveloperDirectory await:&error] != nil; if (!xcodeAvailable) { [logger.error logFormat:@"Xcode is not available, idb will not be able to use Simulators: %@", error]; error = nil; } FBFuture<NSNumber *> *signalled = [FBFuture race:@[ signalHandlerFuture(SIGINT, @"Signalled: SIGINT", logger), signalHandlerFuture(SIGTERM, @"Signalled: SIGTERM", logger), ]]; FBFuture<NSNull *> *companionCompleted = [GetCompanionCompletedFuture(userDefaults, xcodeAvailable, logger) await:&error]; if (!companionCompleted) { [logger.error log:error.localizedDescription]; return 1; } FBFuture<NSNull *> *completed = [FBFuture race:@[ companionCompleted, signalled, ]]; if (completed.error) { [logger.error log:completed.error.localizedDescription]; return 1; } id result = [completed await:&error]; if (!result) { [logger.error log:error.localizedDescription]; return 1; } if (companionCompleted.state == FBFutureStateCancelled) { [logger logFormat:@"Responding to termination of idb with signo %@", result]; FBFuture<NSNull *> *cancellation = [companionCompleted cancel]; result = [cancellation await:&error]; if (!result) { [logger.error log:error.localizedDescription]; return 1; } } } return 0; }