FBSimulatorControl/Commands/FBSimulatorProcessSpawnCommands.m (122 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 "FBSimulatorProcessSpawnCommands.h"
#import <CoreSimulator/SimDevice.h>
#import <FBControlCore/FBControlCore.h>
#import "FBSimulator+Private.h"
#import "FBSimulator.h"
#import "FBSimulatorError.h"
@interface FBSimulatorProcessSpawnCommands ()
@property (nonatomic, weak, readonly) FBSimulator *simulator;
@end
@implementation FBSimulatorProcessSpawnCommands
#pragma mark Initializers
+ (instancetype)commandsWithTarget:(FBSimulator *)targets
{
return [[self alloc] initWithSimulator:targets];
}
- (instancetype)initWithSimulator:(FBSimulator *)simulator
{
self = [super init];
if (!self) {
return nil;
}
_simulator = simulator;
return self;
}
#pragma mark FBSimulatorProcessSpawnCommands Implementation
- (FBFuture<FBProcess *> *)launchProcess:(FBProcessSpawnConfiguration *)configuration
{
FBSimulator *simulator = self.simulator;
return [[configuration.io
attach]
onQueue:simulator.workQueue fmap:^(FBProcessIOAttachment *attachment) {
return [FBSimulatorProcessSpawnCommands
launchProcessWithSimulator:simulator
configuration:configuration
attachment:attachment];
}];
}
#pragma mark Public
+ (NSDictionary<NSString *, id> *)launchOptionsWithArguments:(NSArray<NSString *> *)arguments environment:(NSDictionary<NSString *, NSString *> *)environment waitForDebugger:(BOOL)waitForDebugger
{
NSMutableDictionary<NSString *, id> *options = [NSMutableDictionary dictionary];
options[@"arguments"] = arguments;
options[@"environment"] = environment ? environment: @{@"__SOME_MAGIC__" : @"__IS_ALIVE__"};
if (waitForDebugger) {
options[@"wait_for_debugger"] = @1;
}
return options;
}
#pragma mark Private
+ (FBFuture<FBProcess *> *)launchProcessWithSimulator:(FBSimulator *)simulator configuration:(FBProcessSpawnConfiguration *)configuration attachment:(FBProcessIOAttachment *)attachment
{
// Prepare captured futures
id<FBControlCoreLogger> logger = simulator.logger;
FBMutableFuture<NSNumber *> *launchFuture = [FBMutableFuture futureWithNameFormat:@"Launch of %@ on %@", configuration.launchPath, simulator.udid];
FBMutableFuture<NSNumber *> *statLoc = [FBMutableFuture futureWithNameFormat:@"Process completion of %@ on %@", configuration.launchPath, simulator.udid];
FBMutableFuture<NSNumber *> *exitCode = [FBMutableFuture futureWithNameFormat:@"Process exit of %@ on %@", configuration.launchPath, simulator.udid];
FBMutableFuture<NSNumber *> *signal = [FBMutableFuture futureWithNameFormat:@"Process signal of %@ on %@", configuration.launchPath, simulator.udid];
// Get the Options
NSDictionary<NSString *, id> *options = [self
simDeviceLaunchOptionsWithSimulator:simulator
launchPath:configuration.launchPath
arguments:configuration.arguments
environment:configuration.environment
waitForDebugger:NO
stdOut:attachment.stdOut
stdErr:attachment.stdErr
mode:configuration.mode];
// The Process launches and terminates asynchronously.
[simulator.device
spawnAsyncWithPath:configuration.launchPath
options:options
terminationQueue:simulator.workQueue
terminationHandler:^(int stat_loc) {
// Notify that we're done with the process to each of the futures.
[FBProcessSpawnCommandHelpers
resolveProcessFinishedWithStatLoc:stat_loc
inTeardownOfIOAttachment:attachment
statLocFuture:statLoc
exitCodeFuture:exitCode
signalFuture:signal
processIdentifier:[launchFuture.result intValue]
configuration:configuration
queue:simulator.workQueue
logger:logger];
// Close any open file handles that we have.
// This is important because otherwise any reader will stall forever.
// The SimDevice APIs do not automatically close any file descriptor passed into them, so we need to do this on it's behalf.
// This would not be an issue if using simctl directly, as the stdout/stderr of the simctl process would close when the simctl process terminates.
// However, using the simctl approach, we don't get the pid of the spawned process, this is merely logged internally.
// Failing to close this end of the file descriptor would lead to the write-end of any pipe to not be closed and therefore it would leak.
close(attachment.stdOut.fileDescriptor);
close(attachment.stdErr.fileDescriptor);
}
completionQueue:simulator.workQueue
completionHandler:^(NSError *innerError, pid_t processIdentifier){
if (innerError) {
[launchFuture resolveWithError:innerError];
} else {
[launchFuture resolveWithResult:@(processIdentifier)];
}
}];
// Map to the FBProcess implementation.
return [launchFuture
onQueue:simulator.workQueue map:^(NSNumber *processIdentifierNumber) {
// Wrap in the container object
pid_t processIdentifier = processIdentifierNumber.intValue;
return [[FBProcess alloc] initWithProcessIdentifier:processIdentifier statLoc:statLoc exitCode:exitCode signal:signal configuration:configuration queue:simulator.workQueue];
}];
}
+ (NSDictionary<NSString *, id> *)simDeviceLaunchOptionsWithSimulator:(FBSimulator *)simulator launchPath:(NSString *)launchPath arguments:(NSArray<NSString *> *)arguments environment:(NSDictionary<NSString *, NSString *> *)environment waitForDebugger:(BOOL)waitForDebugger stdOut:(nullable FBProcessStreamAttachment *)stdOut stdErr:(nullable FBProcessStreamAttachment *)stdErr mode:(FBProcessSpawnMode)mode
{
// argv[0] should be launch path of the process. SimDevice does not do this automatically, so we need to add it.
arguments = [@[launchPath] arrayByAddingObjectsFromArray:arguments];
NSMutableDictionary<NSString *, id> *options = [[self launchOptionsWithArguments:arguments environment:environment waitForDebugger:waitForDebugger] mutableCopy];
if (stdOut){
options[@"stdout"] = @(stdOut.fileDescriptor);
}
if (stdErr) {
options[@"stderr"] = @(stdErr.fileDescriptor);
}
options[@"standalone"] = @([self shouldLaunchStandaloneOnSimulator:simulator mode:mode]);
return [options copy];
}
+ (BOOL)shouldLaunchStandaloneOnSimulator:(FBSimulator *)simulator mode:(FBProcessSpawnMode)mode
{
// Standalone means "launch directly, not via launchd"
switch (mode) {
case FBProcessSpawnModeLaunchd:
return NO;
case FBProcessSpawnModePosixSpawn:
return YES;
default:
// Default behaviour is to use launchd if booted, otherwise use standalone.
return simulator.state != FBiOSTargetStateBooted;
}
}
@end