FBControlCore/Processes/FBProcessTerminationStrategy.m (110 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 "FBProcessTerminationStrategy.h" #import "FBProcessFetcher.h" #import "FBProcessInfo.h" #import "FBControlCoreError.h" #import "FBControlCoreLogger.h" #import "FBControlCoreGlobalConfiguration.h" @implementation FBControlCoreError (FBProcessTerminationStrategy) - (instancetype)attachProcessInfoForIdentifier:(pid_t)processIdentifier processFetcher:(FBProcessFetcher *)processFetcher { return [self extraInfo:[NSString stringWithFormat:@"%d_process", processIdentifier] value:[processFetcher processInfoFor:processIdentifier] ?: @"No Process Info"]; } @end static NSTimeInterval ProcessTableRemovalTimeout = 20.0; static const FBProcessTerminationStrategyConfiguration FBProcessTerminationStrategyConfigurationDefault = { .signo = SIGKILL, .options = FBProcessTerminationStrategyOptionsCheckProcessExistsBeforeSignal | FBProcessTerminationStrategyOptionsCheckDeathAfterSignal | FBProcessTerminationStrategyOptionsBackoffToSIGKILL, }; @interface FBProcessTerminationStrategy () @property (nonatomic, assign, readonly) FBProcessTerminationStrategyConfiguration configuration; @property (nonatomic, strong, readonly) FBProcessFetcher *processFetcher; @property (nonatomic, strong, readonly) dispatch_queue_t workQueue; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @end @implementation FBProcessTerminationStrategy #pragma mark Initializers + (instancetype)strategyWithConfiguration:(FBProcessTerminationStrategyConfiguration)configuration processFetcher:(FBProcessFetcher *)processFetcher workQueue:(dispatch_queue_t)workQueue logger:(id<FBControlCoreLogger>)logger; { return [[FBProcessTerminationStrategy alloc] initWithConfiguration:configuration processFetcher:processFetcher workQueue:workQueue logger:logger]; } + (instancetype)strategyWithProcessFetcher:(FBProcessFetcher *)processFetcher workQueue:(dispatch_queue_t)workQueue logger:(id<FBControlCoreLogger>)logger { return [self strategyWithConfiguration:FBProcessTerminationStrategyConfigurationDefault processFetcher:processFetcher workQueue:workQueue logger:logger]; } - (instancetype)initWithConfiguration:(FBProcessTerminationStrategyConfiguration)configuration processFetcher:(FBProcessFetcher *)processFetcher workQueue:(dispatch_queue_t)workQueue logger:(id<FBControlCoreLogger>)logger { NSParameterAssert(processFetcher); NSAssert(configuration.signo > 0 && configuration.signo < 32, @"Signal must be greater than 0 (SIGHUP) and less than 32 (SIGUSR2) was %d", configuration.signo); self = [super init]; if (!self) { return nil; } _configuration = configuration; _processFetcher = processFetcher; _workQueue = workQueue; _logger = logger; return self; } #pragma mark Public Methods - (FBFuture<NSNull *> *)killProcessIdentifier:(pid_t)processIdentifier { BOOL checkExists = (self.configuration.options & FBProcessTerminationStrategyOptionsCheckProcessExistsBeforeSignal) == FBProcessTerminationStrategyOptionsCheckProcessExistsBeforeSignal; if (checkExists && [self.processFetcher processInfoFor:processIdentifier] == nil) { return [[FBControlCoreError describeFormat:@"Could not find that process %d exists", processIdentifier] failFuture]; } // Kill the process with kill(2). [self.logger.debug logFormat:@"Killing %d", processIdentifier]; if (kill(processIdentifier, self.configuration.signo) != 0) { return [[FBControlCoreError describeFormat:@"Failed to kill %d: '%s'", processIdentifier, strerror(errno)] failFuture]; } BOOL checkDeath = (self.configuration.options & FBProcessTerminationStrategyOptionsCheckDeathAfterSignal) == FBProcessTerminationStrategyOptionsCheckDeathAfterSignal; if (!checkDeath) { [self.logger.debug logFormat:@"Killed %d", processIdentifier]; return FBFuture.empty; } // It may take some time for the process to have truly died, so wait for it to be so. // If this is a SIGKILL and it's taken a while for the process to dissapear, perhaps the process isn't // well behaved when responding to other terminating signals. // There's nothing more than can be done with a SIGKILL. [self.logger.debug logFormat:@"Waiting on %d to dissappear from the process table", processIdentifier]; return [[[self onQueue:self.workQueue waitForProcessIdentifierToDie:processIdentifier processFetcher:self.processFetcher] timeout:ProcessTableRemovalTimeout waitingFor:@"Process %d to be removed from the process table", processIdentifier] onQueue:self.workQueue chain:^FBFuture *(FBFuture *future) { if (future.result) { [self.logger.debug logFormat:@"Process %d terminated", processIdentifier]; return FBFuture.empty; } BOOL backoff = (self.configuration.options & FBProcessTerminationStrategyOptionsBackoffToSIGKILL) == FBProcessTerminationStrategyOptionsBackoffToSIGKILL; if (self.configuration.signo == SIGKILL || !backoff) { return [[[FBControlCoreError describeFormat:@"Timed out waiting for %d to dissapear from the process table", processIdentifier] attachProcessInfoForIdentifier:processIdentifier processFetcher:self.processFetcher] failFuture]; } // Try with SIGKILL instead. FBProcessTerminationStrategyConfiguration configuration = self.configuration; configuration.signo = SIGKILL; [self.logger.debug logFormat:@"Backing off kill of %d to SIGKILL", processIdentifier]; return [[[self strategyWithConfiguration:configuration] killProcessIdentifier:processIdentifier] rephraseFailure:@"Attempted to SIGKILL %d after failed kill with signo %d", processIdentifier, self.configuration.signo]; }]; } #pragma mark Private - (FBProcessTerminationStrategy *)strategyWithConfiguration:(FBProcessTerminationStrategyConfiguration)configuration { return [FBProcessTerminationStrategy strategyWithConfiguration:configuration processFetcher:self.processFetcher workQueue:self.workQueue logger:self.logger]; } - (FBFuture<NSNull *> *)onQueue:(dispatch_queue_t)queue waitForProcessIdentifierToDie:(pid_t)processIdentifier processFetcher:(FBProcessFetcher *)processFetcher { return [FBFuture onQueue:queue resolveWhen:^ BOOL { FBProcessInfo *polledProcess = [processFetcher processInfoFor:processIdentifier]; return polledProcess == nil; }]; } @end