FBControlCore/Tasks/FBProcess.m (225 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 "FBProcess.h"
#include <spawn.h>
#import "FBCollectionInformation.h"
#import "FBControlCoreError.h"
#import "FBControlCoreLogger.h"
#import "FBDataBuffer.h"
#import "FBDataConsumer.h"
#import "FBFileWriter.h"
#import "FBProcess.h"
#import "FBProcessIO.h"
#import "FBProcessSpawnCommands.h"
#import "FBProcessSpawnConfiguration.h"
#import "FBProcessStream.h"
static BOOL AddOutputFileActions(posix_spawn_file_actions_t *fileActions, FBProcessStreamAttachment *attachment, int targetFileDescriptor, NSError **error)
{
if (!attachment) {
return YES;
}
NSCParameterAssert(attachment.mode == FBProcessStreamAttachmentModeOutput);
// dup the write end of the pipe to the target file descriptor i.e. stdout
// Files do not need to be closed in the launched process as POSIX_SPAWN_CLOEXEC_DEFAULT does this for us.
int sourceFileDescriptor = attachment.fileDescriptor;
int status = posix_spawn_file_actions_adddup2(fileActions, sourceFileDescriptor, targetFileDescriptor);
if (status != 0) {
return [[FBControlCoreError
describeFormat:@"Failed to dup input %d, to %d: %s", sourceFileDescriptor, targetFileDescriptor, strerror(status)]
failBool:error];
}
return YES;
}
static BOOL AddInputFileActions(posix_spawn_file_actions_t *fileActions, FBProcessStreamAttachment *attachment, int targetFileDescriptor, NSError **error)
{
if (!attachment) {
return YES;
}
NSCParameterAssert(attachment.mode == FBProcessStreamAttachmentModeInput);
// dup the read end of the pipe to the target file descriptor i.e. stdin
// Files do not need to be closed in the launched process as POSIX_SPAWN_CLOEXEC_DEFAULT does this for us.
int sourceFileDescriptor = attachment.fileDescriptor;
int status = posix_spawn_file_actions_adddup2(fileActions, sourceFileDescriptor, targetFileDescriptor);
if (status != 0) {
return [[FBControlCoreError
describeFormat:@"Failed to dup input %d, to %d: %s", sourceFileDescriptor, targetFileDescriptor, strerror(status)]
failBool:error];
}
return YES;
}
@interface FBProcess ()
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@end
@implementation FBProcess
@synthesize configuration = _configuration;
@synthesize exitCode = _exitCode;
@synthesize processIdentifier = _processIdentifier;
@synthesize signal = _signal;
@synthesize statLoc = _statLoc;
#pragma mark Initializers
- (instancetype)initWithProcessIdentifier:(pid_t)processIdentifier statLoc:(FBFuture<NSNumber *> *)statLoc exitCode:(FBFuture<NSNumber *> *)exitCode signal:(FBFuture<NSNumber *> *)signal configuration:(FBProcessSpawnConfiguration *)configuration queue:(dispatch_queue_t)queue
{
self = [super init];
if (!self) {
return nil;
}
_configuration = configuration;
_processIdentifier = processIdentifier;
_exitCode = exitCode;
_signal = signal;
_statLoc = statLoc;
_queue = queue;
return self;
}
+ (FBFuture<FBProcess *> *)launchProcessWithConfiguration:(FBProcessSpawnConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.task", DISPATCH_QUEUE_SERIAL);
return [[configuration.io
attach]
onQueue:queue fmap:^(FBProcessIOAttachment *attachment) {
// Everything is setup, launch the process now.
NSError *error = nil;
FBProcess *process = [FBProcess processWithConfiguration:configuration attachment:attachment queue:queue logger:logger error:&error];
if (!process) {
return [FBFuture futureWithError:error];
}
return [FBFuture futureWithResult:process];
}];
}
#pragma mark Public Methods
- (FBFuture<NSNumber *> *)exitedWithCodes:(NSSet<NSNumber *> *)acceptableExitCodes
{
return [[FBMutableFuture.future
resolveFromFuture:self.exitCode]
onQueue:self.queue fmap:^(NSNumber *exitCode) {
return [[FBProcess confirmExitCode:exitCode.intValue isAcceptable:acceptableExitCodes] mapReplace:exitCode];
}];
}
- (FBFuture<NSNumber *> *)sendSignal:(int)signo
{
return [[FBFuture
onQueue:self.queue resolve:^{
// Do not kill if the process is already dead.
if (self.statLoc.hasCompleted) {
return self.statLoc;
}
kill(self.processIdentifier, signo);
return self.statLoc;
}]
mapReplace:@(signo)];
}
- (FBFuture<NSNumber *> *)sendSignal:(int)signo backingOffToKillWithTimeout:(NSTimeInterval)timeout logger:(id<FBControlCoreLogger>)logger
{
return [[[self
sendSignal:signo]
onQueue:self.queue timeout:timeout handler:^{
[logger logFormat:@"Process %d didn't exit after wait for %f seconds for sending signal %d, sending SIGKILL now.", self.processIdentifier, timeout, signo];
return [self sendSignal:SIGKILL];
}]
mapReplace:@(signo)];
}
#pragma mark Properties
- (nullable id)stdIn
{
return [self.configuration.io.stdIn contents];
}
- (nullable id)stdOut
{
return [self.configuration.io.stdOut contents];
}
- (nullable id)stdErr
{
return [self.configuration.io.stdErr contents];
}
#pragma mark Private
+ (FBFuture<NSNull *> *)confirmExitCode:(int)exitCode isAcceptable:(NSSet<NSNumber *> *)acceptableExitCodes
{
// If exit codes are defined, check them.
if (acceptableExitCodes == nil) {
return FBFuture.empty;
}
if ([acceptableExitCodes containsObject:@(exitCode)]) {
return FBFuture.empty;
}
return [[FBControlCoreError
describeFormat:@"Exit Code %d is not acceptable %@", exitCode, [FBCollectionInformation oneLineDescriptionFromArray:acceptableExitCodes.allObjects]]
failFuture];
}
+ (FBProcess *)processWithConfiguration:(FBProcessSpawnConfiguration *)configuration attachment:(FBProcessIOAttachment *)attachment queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
// Convert the arguments to the argv expected by posix_spawn
NSArray<NSString *> *arguments = configuration.arguments;
char *argv[arguments.count + 2]; // 0th arg is launch path, last arg is NULL
argv[0] = (char *) configuration.launchPath.UTF8String;
argv[arguments.count + 1] = NULL;
for (NSUInteger index = 0; index < arguments.count; index++) {
argv[index + 1] = (char *) arguments[index].UTF8String; // Offset by the launch path arg.
}
// Convert the environment to the envp expected by posix_spawn
NSDictionary<NSString *, NSString *> *environment = configuration.environment;
NSArray<NSString *> *environmentNames = environment.allKeys;
char *envp[environment.count + 1];
envp[environment.count] = NULL;
for (NSUInteger index = 0; index < environmentNames.count; index++) {
NSString *name = environmentNames[index];
NSString *value = [NSString stringWithFormat:@"%@=%@", name, environment[name]];
envp[index] = (char *) value.UTF8String;
}
// Convert the file descriptors
posix_spawn_file_actions_t fileActions;
posix_spawn_file_actions_init(&fileActions);
if (!AddInputFileActions(&fileActions, attachment.stdIn, STDIN_FILENO, error)) {
return nil;
}
if (!AddOutputFileActions(&fileActions, attachment.stdOut, STDOUT_FILENO, error)) {
return nil;
}
if (!AddOutputFileActions(&fileActions, attachment.stdErr, STDERR_FILENO, error)) {
return nil;
}
// Make the spawn attributes
posix_spawnattr_t spawnAttributes;
posix_spawnattr_init(&spawnAttributes);
// No signals in the child process will be masked from whatever is set in the current process.
sigset_t mask;
sigemptyset(&mask);
posix_spawnattr_setsigmask(&spawnAttributes, &mask);
// All signals in the new process should have the default disposition.
sigfillset(&mask);
posix_spawnattr_setsigdefault(&spawnAttributes, &mask);
// Closes all file descriptors in the child that aren't duped. This prevents any file descriptors other than the ones we define being inherited by children.
posix_spawnattr_setflags(&spawnAttributes, POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);
pid_t processIdentifier;
int status = posix_spawn(&processIdentifier, argv[0], &fileActions, &spawnAttributes, argv, envp);
posix_spawn_file_actions_destroy(&fileActions);
posix_spawnattr_destroy(&spawnAttributes);
if (status != 0) {
return [[FBControlCoreError
describeFormat:@"Failed to launch %@ with error %s", configuration, strerror(status)]
fail:error];
}
[logger logFormat:@"%@ Launched with pid %d", configuration.processName, processIdentifier];
FBMutableFuture<NSNumber *> *statLoc = FBMutableFuture.future;
FBMutableFuture<NSNumber *> *exitCode = FBMutableFuture.future;
FBMutableFuture<NSNumber *> *signal = FBMutableFuture.future;
[self resolveProcessCompletion:processIdentifier attachment:attachment statLoc:statLoc exitCode:exitCode signal:signal configuration:configuration logger:logger];
return [[self alloc] initWithProcessIdentifier:processIdentifier statLoc:statLoc exitCode:exitCode signal:signal configuration:configuration queue:queue];
}
+ (void)resolveProcessCompletion:(pid_t)processIdentifier attachment:(FBProcessIOAttachment *)attachment statLoc:(FBMutableFuture<NSNumber *> *)statLoc exitCode:(FBMutableFuture<NSNumber *> *)exitCode signal:(FBMutableFuture<NSNumber *> *)signal configuration:(FBProcessSpawnConfiguration *)configuration logger:(id<FBControlCoreLogger>)logger
{
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbcontrolcore.task.posix_spawn.wait", DISPATCH_QUEUE_SERIAL);
dispatch_source_t source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_PROC,
(uintptr_t) processIdentifier,
DISPATCH_PROC_EXIT,
queue
);
dispatch_source_set_event_handler(source, ^{
int status = 0;
if (waitpid(processIdentifier, &status, WNOHANG) == -1) {
[logger logFormat:@"Failed to get the exit status with waitpid: %s", strerror(errno)];
}
// Resolve all of the related process finshed futures now, so that they do not need asynchronous resolution.
[FBProcessSpawnCommandHelpers
resolveProcessFinishedWithStatLoc:status
inTeardownOfIOAttachment:attachment
statLocFuture:statLoc
exitCodeFuture:exitCode
signalFuture:signal
processIdentifier:processIdentifier
configuration:configuration
queue:queue
logger:logger];
// We only need a single notification and the dispatch_source must be retained until we resolve the future.
// Cancelling the source at the end will release the source as the event handler will no longer be referenced.
dispatch_cancel(source);
});
dispatch_resume(source);
}
#pragma mark NSObject
- (NSString *)description
{
return [NSString stringWithFormat:@"Process %@ | pid %d | State %@", self.configuration.description, self.processIdentifier, self.statLoc];
}
@end