FBControlCore/Processes/FBProcessFetcher.m (283 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 "FBProcessFetcher.h"
#import "FBControlCoreError.h"
#import "FBFuture.h"
#include <libproc.h>
#include <limits.h>
#include <string.h>
#include <sys/sysctl.h>
#import "FBProcessInfo.h"
#define PID_MAX 99999
#pragma mark Calling libproc
typedef BOOL(^ProcessIterator)(pid_t pid);
typedef int(^LibProcCaller)(void);
static void IterateWith(pid_t *pidBuffer, size_t pidBufferSize, ProcessIterator iterator, LibProcCaller caller)
{
int actualSize = caller();
if (actualSize < 1) {
return;
}
for (int index = 0; index < actualSize; index++) {
pid_t processIdentifier = *(pidBuffer + index);
if (!iterator(processIdentifier)) {
break;
}
}
}
static void IterateAllProcesses(pid_t *pidBuffer, size_t pidBufferSize, ProcessIterator iterator)
{
IterateWith(pidBuffer, pidBufferSize, iterator, ^ int () {
return proc_listallpids(pidBuffer, (int) pidBufferSize);
});
}
static void IterateSubprocessesOf(pid_t *pidBuffer, size_t pidBufferSize, pid_t parent, ProcessIterator iterator)
{
IterateWith(pidBuffer, pidBufferSize, iterator, ^ int () {
return proc_listchildpids(parent, pidBuffer, (int) pidBufferSize);
});
}
static void IterateOpenFiles(pid_t *pidBuffer, size_t pidBufferSize, const char *path, ProcessIterator iterator)
{
IterateWith(pidBuffer, pidBufferSize,iterator, ^ int () {
return proc_listpidspath(
PROC_LISTPIDSPATH_PATH_IS_VOLUME,
PROC_ALL_PIDS,
path,
0,
pidBuffer,
(int) pidBufferSize
);
});
}
static inline FBProcessInfo *ProcessInfoForProcessIdentifier(pid_t processIdentifier, char *buffer, size_t bufferSize)
{
// Much of the layout information here comes from libtop.c in Apple's top(1) Open Source implementation.
int name[3] = {CTL_KERN, KERN_PROCARGS2, processIdentifier};
size_t actualSize = bufferSize;
if (sysctl(name, 3, buffer, &actualSize, NULL, 0) == -1) {
return nil;
}
if (actualSize == 0) {
return nil;
}
// First Position is argc.
const char *startPosition = buffer;
const int argc = *startPosition;
// If argc isn't 1 or more, something is wrong
if (argc < 1) {
return nil;
}
// launch path starts above argc
char *currentPosition = (char *) startPosition + sizeof(int);
NSString *launchPath = [[NSString alloc] initWithCString:currentPosition encoding:NSASCIIStringEncoding];
currentPosition += strlen(currentPosition);
currentPosition += 1;
// Move through the padding to get to the the argv
while (*currentPosition == '\0') {
currentPosition++;
}
// Enumerate up to the value of argc
NSMutableArray *arguments = [NSMutableArray array];
for (int index = 0; index < argc; index++) {
// Create Objective-C String from current position.
NSString *argument = [[NSString alloc] initWithCString:currentPosition encoding:NSASCIIStringEncoding];
[arguments addObject:argument];
// Move the current string position passed the null-character.
currentPosition += strlen(currentPosition);
currentPosition += 1;
}
// Now the environment is here
NSMutableDictionary *environment = [NSMutableDictionary dictionary];
while (*currentPosition != '\0') {
NSString *string = [[NSString alloc] initWithCString:currentPosition encoding:NSASCIIStringEncoding];
NSArray *tokens = [string componentsSeparatedByString:@"="];
// If we don't get 2 tokens, something is malformed.
if (tokens.count != 2) {
break;
}
environment[tokens[0]] = tokens[1];
// Move the current string position passed the null-character.
currentPosition += strlen(currentPosition);
currentPosition += 1;
}
FBProcessInfo *process = [[FBProcessInfo alloc]
initWithProcessIdentifier:processIdentifier
launchPath:launchPath
arguments:arguments
environment:environment];
return process;
}
static inline BOOL ProcInfoForProcessIdentifier(pid_t processIdentifier, struct kinfo_proc* procOut)
{
size_t size = sizeof(struct kinfo_proc);
int name[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, processIdentifier };
if (sysctl(name, 4, procOut, &size, NULL, 0) == -1) {
return NO;
}
return YES;
}
static BOOL ProcessNameForProcessIdentifier(pid_t processIdentifier, char *buffer, size_t bufferSize)
{
return proc_name(processIdentifier, buffer, (uint32_t) bufferSize) > 1;
}
@interface FBProcessFetcher ()
@property (nonatomic, assign, readonly) size_t argumentBufferSize;
@property (nonatomic, assign, readonly) char *argumentBuffer;
@property (nonatomic, assign, readonly) size_t pidBufferSize;
@property (nonatomic, assign, readonly) pid_t *pidBuffer;
@end
@implementation FBProcessFetcher
#pragma mark Lifecycle
static size_t MaxArgumentBufferSize = ARG_MAX; // A temporary value that is filled on load
static size_t const MaxPidBufferSize = 5568 * 2 * sizeof(int); // From 'ulimit -u', but twice as large, in ints.
+ (void)load
{
int name[2] = {CTL_KERN, KERN_ARGMAX};
size_t size = sizeof(MaxArgumentBufferSize);
int status = sysctl(name, 2, &MaxArgumentBufferSize, &size, NULL, 0);
NSAssert(status != -1, @"Failed to get the KERN_ARGMAX from sysctl %s", strerror(errno));
}
- (instancetype)init
{
self = [super init];
if (!self) {
return nil;
}
_argumentBufferSize = MaxArgumentBufferSize;
_argumentBuffer = malloc(_argumentBufferSize);
_pidBufferSize = MaxPidBufferSize;
_pidBuffer = malloc(_pidBufferSize);
return self;
}
- (void)dealloc
{
free(_argumentBuffer);
free(_pidBuffer);
}
#pragma mark Queries
- (nullable FBProcessInfo *)processInfoFor:(pid_t)processIdentifier
{
return ProcessInfoForProcessIdentifier(
processIdentifier,
self.argumentBuffer,
self.argumentBufferSize
);
}
- (NSArray<FBProcessInfo *> *)subprocessesOf:(pid_t)parent
{
NSMutableArray *subprocesses = [NSMutableArray array];
IterateSubprocessesOf(self.pidBuffer, self.pidBufferSize, parent, ^ BOOL (pid_t pid) {
FBProcessInfo *info = [self processInfoFor:pid];
if (info) {
[subprocesses addObject:info];
}
return YES;
});
return [subprocesses copy];
}
- (NSArray<FBProcessInfo *> *)processesWithProcessName:(NSString *)processName
{
NSMutableArray *subprocesses = [NSMutableArray array];
size_t bufferSize = self.argumentBufferSize;
char *buffer = self.argumentBuffer;
const char *needle = processName.UTF8String;
IterateAllProcesses(self.pidBuffer, self.pidBufferSize, ^ BOOL (pid_t pid) {
if (!ProcessNameForProcessIdentifier(pid, buffer, bufferSize)) {
return YES;
}
if (strcmp(needle, buffer) != 0) {
return YES;
}
FBProcessInfo *info = [self processInfoFor:pid];
if (!info) {
return YES;
}
[subprocesses addObject:info];
return YES;
});
return [subprocesses copy];
}
- (pid_t)subprocessOf:(pid_t)parent withName:(NSString *)needleString
{
__block pid_t foundProcess = -1;
size_t argumentBufferSize = self.argumentBufferSize;
char *argumentBuffer = self.argumentBuffer;
const char *needle = needleString.UTF8String;
IterateSubprocessesOf(self.pidBuffer, self.pidBufferSize, parent, ^ BOOL (pid_t pid) {
if (proc_name(pid, argumentBuffer, (uint32_t) argumentBufferSize) == -1) {
return YES;
}
if (strstr(argumentBuffer, needle) == NULL) {
return YES;
}
foundProcess = pid;
return NO;
});
return foundProcess;
}
- (pid_t)processWithOpenFileTo:(const char *)filename
{
__block pid_t processIdentifier = -1;
IterateOpenFiles(self.pidBuffer, self.pidBufferSize, filename, ^ BOOL (pid_t pid) {
processIdentifier = pid;
return NO;
});
return processIdentifier;
}
- (pid_t)parentOf:(pid_t)child
{
struct kinfo_proc proc;
if (!ProcInfoForProcessIdentifier(child, &proc)) {
return -1;
}
return proc.kp_eproc.e_ppid;
}
- (struct kinfo_proc) fetchProcessInfo:(pid_t)processIdentifier error:(NSError **)error
{
struct kinfo_proc proc_info;
if (!ProcInfoForProcessIdentifier(processIdentifier, &proc_info) || proc_info.kp_proc.p_pid != processIdentifier) {
[[FBControlCoreError
describeFormat:@"Failed fetching process info for (pid %d)", processIdentifier]
failBool:error];
}
return proc_info;
}
- (BOOL) isProcessRunning:(pid_t)processIdentifier error:(NSError **)error
{
struct kinfo_proc proc_info = [self fetchProcessInfo:processIdentifier error:error];
return *error == nil && proc_info.kp_proc.p_stat == SRUN;
}
- (BOOL) isProcessStopped:(pid_t)processIdentifier error:(NSError **)error
{
struct kinfo_proc proc_info = [self fetchProcessInfo:processIdentifier error:error];
return *error == nil && proc_info.kp_proc.p_stat == SSTOP;
}
- (BOOL) isDebuggerAttachedTo:(pid_t)processIdentifier error:(NSError **)error
{
struct kinfo_proc proc_info = [self fetchProcessInfo:processIdentifier error:error];
// When a debugger (a.k.a tracer) attaches to the test proccess, the parent of tracee will
// change to tracer's pid with the original parent pid being store in `p_oppid`.
// We detect debugger attachment by checking that parent pid has changed.
return *error == nil && proc_info.kp_proc.p_oppid != 0 && proc_info.kp_eproc.e_ppid != proc_info.kp_proc.p_oppid;
}
+ (FBFuture<NSNull *> *) waitForDebuggerToAttachAndContinueFor:(pid_t)processIdentifier
{
FBProcessFetcher *processFetcher = [[FBProcessFetcher alloc] init];
// Report from the current queue, but wait in a special queue.
dispatch_queue_t waitQueue = dispatch_queue_create("com.facebook.corecontrol.debugger_wait", DISPATCH_QUEUE_SERIAL);
return [FBFuture
onQueue:waitQueue resolveOrFailWhen:^FBFutureLoopState (NSError **error){
if (
[processFetcher isDebuggerAttachedTo:processIdentifier error:error] &&
[processFetcher isProcessRunning:processIdentifier error:error]
) {
return FBFutureLoopFinished;
} else if (*error != nil){
return FBFutureLoopFailed;
} else {
return FBFutureLoopContinue;
}
}];
}
+ (FBFuture<NSNull *> *) waitStopSignalForProcess:(pid_t) processIdentifier
{
FBProcessFetcher *processFetcher = [[FBProcessFetcher alloc] init];
dispatch_queue_t waitQueue = dispatch_queue_create("com.facebook.corecontrol.wait_for_stop", DISPATCH_QUEUE_SERIAL);
return [FBFuture
onQueue:waitQueue resolveOrFailWhen:^FBFutureLoopState (NSError **error){
if (
[processFetcher isProcessStopped:processIdentifier error:error]
) {
return FBFutureLoopFinished;
} else if (*error != nil){
return FBFutureLoopFailed;
} else {
return FBFutureLoopContinue;
}
}];
}
@end