FBDeviceControl/Management/FBInstrumentsClient.m (449 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 "FBInstrumentsClient.h"
#import "FBAMDServiceConnection.h"
#pragma mark DTX Internals
/**
Understanding of the DTXMessage protocol is informed by the [ios_instruments_client project](https://github.com/troybowman/ios_instruments_client)
*/
typedef struct
{
uint32 magic;
uint32 cb;
uint16 fragmentId;
uint16 fragmentCount;
uint32 length;
uint32 identifier;
uint32 conversationIndex;
uint32 channelCode;
uint32 expectsReply;
} DTXMessageHeader;
typedef struct
{
uint32 flags;
uint32 auxiliaryLength;
uint64 totalLength;
} DTXMessagePayloadHeader;
#pragma mark Object Internals
typedef struct {
BOOL success;
uint32 messageIdentifier;
uint32 channelCode;
id returnValue;
NSArray<id> *auxillaryValues;
} ResponsePayload;
typedef struct {
NSString *selector;
NSArray<NSData *> *argumentsData;
uint32 messageIdentifier;
uint32 channelCode;
BOOL expectsReply;
} RequestPayload;
static const ResponsePayload InvalidResponsePayload = {
.success = NO,
.messageIdentifier = 0,
.channelCode = 0,
.returnValue = nil,
.auxillaryValues = nil,
};
@interface FBInstrumentsClient ()
@property (nonatomic, assign, readwrite) uint32 lastMessageIdentifier;
@property (nonatomic, assign, readwrite) int32_t lastChannelIdentifier;
@property (nonatomic, copy, readonly) NSDictionary<NSString *, id> *channels;
@property (nonatomic, strong, readonly) FBAMDServiceConnection *connection;
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger;
@end
@implementation FBInstrumentsClient
#pragma mark Initializers
+ (FBFuture<FBInstrumentsClient *> *)instrumentsClientWithServiceConnection:(FBAMDServiceConnection *)connection logger:(id<FBControlCoreLogger>)logger
{
dispatch_queue_t queue = dispatch_queue_create("com.facebook.fbdevicecontrol.fbinstrumentsclient", DISPATCH_QUEUE_SERIAL);
return [FBFuture
onQueue:queue resolveValue:^ FBInstrumentsClient * (NSError **error) {
uint32 responseMessageIdentifier = 0;
NSDictionary<NSString *, id> *channels = [FBInstrumentsClient getAvailableChannels:connection responseMessageIdentifierOut:&responseMessageIdentifier error:error];
if (!channels) {
return nil;
}
return [[self alloc] initWithConnection:connection channels:channels lastMessageIdentifier:responseMessageIdentifier queue:queue logger:logger];
}];
}
- (instancetype)initWithConnection:(FBAMDServiceConnection *)connection channels:(NSDictionary<NSString *, id> *)channels lastMessageIdentifier:(uint32)lastMessageIdentifier queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
self = [super init];
if (!self) {
return nil;
}
_connection = connection;
_channels = channels;
_queue = queue;
_logger = logger;
_lastMessageIdentifier = lastMessageIdentifier;
_lastChannelIdentifier = 0;
return self;
}
#pragma mark Public Methods
static NSString *const ProcessControlChannel = @"com.apple.instruments.server.services.processcontrol";
- (FBFuture<NSNumber *> *)launchApplication:(FBApplicationLaunchConfiguration *)configuration
{
return [FBFuture
onQueue:self.queue resolveValue:^ NSNumber * (NSError **error) {
NSDictionary<NSString *, NSNumber *> *options = @{
@"StartSuspendedKey": @(configuration.waitForDebugger),
@"KillExisting": @(configuration.launchMode != FBApplicationLaunchModeFailIfRunning),
};
ResponsePayload response = [self
onChannelIdentifier:ProcessControlChannel
performSelector:@"launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:"
argumentsData:@[
[FBInstrumentsClient argumentDataForArgument:@""], // devicePath:
[FBInstrumentsClient argumentDataForArgument:configuration.bundleID], // bundleIdentifier:
[FBInstrumentsClient argumentDataForArgument:configuration.environment], // environment:
[FBInstrumentsClient argumentDataForArgument:configuration.arguments], // arguments:
[FBInstrumentsClient argumentDataForArgument:options], // options:
]
error:error];
if (response.success == NO) {
return nil;
}
return response.returnValue;
}];
}
- (FBFuture<NSNull *> *)killProcess:(pid_t)processIdentifier
{
return [FBFuture
onQueue:self.queue resolveValue:^ NSNull * (NSError **error) {
ResponsePayload response = [self
onChannelIdentifier:ProcessControlChannel
performSelector:@"killPid:"
argumentsData:@[
[FBInstrumentsClient argumentDataForArgument:@(processIdentifier)], // pid:
]
error:error];
if (response.success == NO) {
return nil;
}
return NSNull.null;
}];
}
#pragma mark Private Class Methods
+ (NSData *)capabilitiesArgumentData
{
static dispatch_once_t onceToken;
static NSData *data;
dispatch_once(&onceToken, ^{
data = [self argumentDataForArgument:@{@"com.apple.private.DTXBlockCompression": @2, @"com.apple.private.DTXConnection": @1}];
});
return data;
}
+ (NSSet<Class> *)supportedReturnSerializerValues
{
static dispatch_once_t onceToken;
static NSSet<Class> *classes;
dispatch_once(&onceToken, ^{
classes = [NSSet setWithArray:@[NSString.class, NSNumber.class, NSDate.class, NSError.class, NSData.class, NSDictionary.class, NSArray.class]];
});
return classes;
}
+ (NSDictionary<NSString *, id> *)getAvailableChannels:(FBAMDServiceConnection *)connection responseMessageIdentifierOut:(uint32 *)responseMessageIdentifierOut error:(NSError **)error
{
RequestPayload request = {
.selector = @"_notifyOfPublishedCapabilities:",
.argumentsData = @[self.capabilitiesArgumentData],
.messageIdentifier = 1,
.channelCode = 0,
.expectsReply = NO,
};
const ResponsePayload response = [self onConnection:connection requestSendAndReceive:request error:error];
if (response.success == NO) {
return nil;
}
NSDictionary<NSString *, NSNumber *> *channels = response.auxillaryValues.firstObject;
if (![channels isKindOfClass:NSDictionary.class]) {
return [[FBControlCoreError
describeFormat:@"%@ is not a dictionary", channels]
fail:error];
}
return channels;
}
+ (ResponsePayload)onConnection:(FBAMDServiceConnection *)connection requestSendAndReceive:(RequestPayload)request error:(NSError **)error
{
NSData *requestData = [self requestDataFromRequest:request];
if (![connection send:requestData error:error]) {
return InvalidResponsePayload;
}
return [self receiveMessage:connection request:request error:error];
}
static const uint32 DTXMessageHeaderMagic = 0x1F3D5B79;
+ (NSData *)requestDataFromRequest:(RequestPayload)request
{
// Arguments are serialized into the auxillary data.
NSData *auxillaryData = [self auxillaryDataFromArgumentsData:request.argumentsData];
// The selector is the "return value" of a request. In a response this will be the return value of the remote method.
NSError *error = nil;
NSData *selectorData = [NSKeyedArchiver archivedDataWithRootObject:request.selector requiringSecureCoding:NO error:&error];
NSAssert(selectorData, @"%@", error);
// Message header is derivable from payload sizing.
DTXMessagePayloadHeader payloadHeader;
payloadHeader.flags = 0x2 | (request.expectsReply ? 0x1000 : 0);
payloadHeader.auxiliaryLength = (uint32) auxillaryData.length;
payloadHeader.totalLength = auxillaryData.length + selectorData.length;
// All messages have a magic number.
DTXMessageHeader messageHeader;
messageHeader.magic = DTXMessageHeaderMagic;
messageHeader.cb = sizeof(DTXMessageHeader);
// We're sending data in a single fragment.
messageHeader.fragmentId = 0;
messageHeader.fragmentCount = 1;
messageHeader.length = (uint32) sizeof(payloadHeader) + (uint32) payloadHeader.totalLength;
messageHeader.identifier = request.messageIdentifier;
messageHeader.conversationIndex = 0;
messageHeader.channelCode = request.channelCode;
messageHeader.expectsReply = (request.expectsReply ? 1 : 0);
// Construct the payload from the slices of data.
// This is not a multi-part message so is:
// 1) The message header, containing the total length of the entire payload.
// 2) The payload header, containing sizing for the aux and selector/return payloads.
// 3) The aux data (arguments to the remote call).
// 4) The selector/return payload (the selector to perform on the remote object).
NSMutableData *data = NSMutableData.data;
[data appendBytes:&messageHeader length:sizeof(messageHeader)];
[data appendBytes:&payloadHeader length:sizeof(payloadHeader)];
[data appendData:auxillaryData];
[data appendData:selectorData];
return data;
}
static const uint64 ArgumentMagic = 0x1F0;
static const uint32 EmptyDictionaryKey = 10;
static const uint32 ObjectArgumentType = 2;
static const uint32 Int32ArgumentType = 3;
+ (NSData *)auxillaryDataFromArgumentsData:(nullable NSArray<NSData *> *)arguments
{
if (arguments == nil) {
return NSData.data;
}
NSMutableData *argumentsData = NSMutableData.data;
for (NSData *argument in arguments) {
[argumentsData appendData:argument];
}
uint64 payloadLength = argumentsData.length;
NSMutableData *data = NSMutableData.data;
[data appendBytes:&ArgumentMagic length:sizeof(ArgumentMagic)];
[data appendBytes:&payloadLength length:sizeof(payloadLength)];
[data appendData:argumentsData];
return data;
}
+ (NSData *)argumentDataForArgument:(id)argument
{
NSError *error = nil;
NSData *argumentData = [NSKeyedArchiver archivedDataWithRootObject:argument requiringSecureCoding:NO error:&error];
NSAssert(argumentData, @"%@", error);
uint32 argumentSize = (uint32) argumentData.length;
NSMutableData *data = NSMutableData.data;
[data appendBytes:&EmptyDictionaryKey length:sizeof(EmptyDictionaryKey)];
[data appendBytes:&ObjectArgumentType length:sizeof(ObjectArgumentType)];
[data appendBytes:&argumentSize length:sizeof(argumentSize)];
[data appendData:argumentData];
return data;
}
+ (NSData *)argumentDataForInt32:(int32_t)value
{
NSMutableData *data = NSMutableData.data;
[data appendBytes:&EmptyDictionaryKey length:sizeof(EmptyDictionaryKey)];
[data appendBytes:&Int32ArgumentType length:sizeof(ObjectArgumentType)];
[data appendBytes:&value length:sizeof(value)];
return data;
}
+ (NSArray<id> *)objectArgumentsFromAuxillaryData:(NSData *)data error:(NSError **)error
{
if (data.length < 16) {
return [[FBControlCoreError
describeFormat:@"Data is of insufficient length %@", data]
fail:error];
}
uint64 magic = 0;
data = [self advanceData:data buffer:&magic length:sizeof(magic)];
uint64 payloadLength = 0;
data = [self advanceData:data buffer:&payloadLength length:sizeof(payloadLength)];
// We need at least the length of the length of the argument data within the buffer.
NSMutableArray<id> *arguments = NSMutableArray.array;
while (data.length > (sizeof(uint32) * 3)) {
uint32 dictionaryKey = 0;
uint32 argumentType = 0;
uint32 argumentLength = 0;
data = [self advanceData:data buffer:&dictionaryKey length:sizeof(dictionaryKey)];
data = [self advanceData:data buffer:&argumentType length:sizeof(argumentType)];
if (argumentType != 2) {
return [[FBControlCoreError
describeFormat:@"Canot decode argument of type %d", argumentType]
fail:error];
}
data = [self advanceData:data buffer:&argumentLength length:sizeof(argumentLength)];
NSData *argumentData = nil;
data = [self advanceData:data dataOut:&argumentData length:argumentLength];
id argument = [NSKeyedUnarchiver unarchivedObjectOfClasses:self.supportedReturnSerializerValues fromData:argumentData error:error];
if (!argument) {
return [[FBControlCoreError
describeFormat:@"Failed to decode argument %@", data]
fail:error];
}
[arguments addObject:argument];
}
return arguments;
}
+ (NSData *)advanceData:(NSData *)data buffer:(void *)buffer length:(size_t)length
{
[data getBytes:buffer length:length];
return [data subdataWithRange:NSMakeRange(length, data.length - length)];
}
+ (NSData *)advanceData:(NSData *)data dataOut:(NSData **)dataOut length:(size_t)length
{
if (dataOut) {
*dataOut = [data subdataWithRange:NSMakeRange(0, length)];
}
return [data subdataWithRange:NSMakeRange(length, data.length - length)];
}
+ (ResponsePayload)receiveMessage:(FBAMDServiceConnection *)connection request:(RequestPayload)request error:(NSError **)error
{
// This header will start the first iteration of the loop, then is overwritten on each iteration.
DTXMessageHeader messageHeader = {
.magic = 0,
.cb = 0,
.fragmentId = 0,
.fragmentCount = UINT16_MAX,
.length = 0,
.identifier = 0,
.conversationIndex = 0,
.channelCode = 0,
.expectsReply = 0,
};
NSMutableData *payloadData = NSMutableData.data;
// Will execute at least once, exiting when there are no more fragments.
while (messageHeader.fragmentId < messageHeader.fragmentCount - 1) {
// Obtain the header payload in this iteration.
if (![connection receive:&messageHeader ofSize:sizeof(messageHeader) error:error]) {
return InvalidResponsePayload;
}
// The data is corrupted in some way if the magic number from the header is missing.
if (messageHeader.magic != DTXMessageHeaderMagic) {
return InvalidResponsePayload;
}
// We should always expect that identifiers are increasing.
if (messageHeader.conversationIndex == 0 && messageHeader.identifier < request.messageIdentifier) {
[[FBControlCoreError
describeFormat:@"Response identifier %d with lower identifier than that requested (%d)", messageHeader.identifier, request.messageIdentifier]
fail:error];
return InvalidResponsePayload;
}
if (messageHeader.conversationIndex == 1 && messageHeader.identifier != request.messageIdentifier) {
[[FBControlCoreError
describeFormat:@"Response identifier %d is not the same as requested identifier (%d)", messageHeader.identifier, request.messageIdentifier]
fail:error];
return InvalidResponsePayload;
}
// First message in a multi-part fragment has no payload, move onto the next fragment which does.
if (messageHeader.fragmentCount > 1 && messageHeader.fragmentId == 0) {
continue;
}
// Consume all data from this fragment and accumilate it.
NSData *fragmentData = [connection receive:messageHeader.length error:error];
if (!fragmentData) {
return InvalidResponsePayload;
}
[payloadData appendData:fragmentData];
}
return [self consumePayloadData:payloadData messageHeader:messageHeader error:error];
}
+ (ResponsePayload)consumePayloadData:(NSData *)payloadData messageHeader:(DTXMessageHeader)messageHeader error:(NSError **)error
{
// There is a single payload header at the start of the payload, even if it is a multi-part message.
DTXMessagePayloadHeader payloadHeader;
payloadData = [self advanceData:payloadData buffer:&payloadHeader length:sizeof(payloadHeader)];
uint8 compression = (payloadHeader.flags & 0xFF000) >> 12;
if (compression != 0) {
return InvalidResponsePayload;
}
// First comes the auxillary data.
size_t auxillaryDataLength = payloadHeader.auxiliaryLength;
NSData *auxillaryData = nil;
if (auxillaryDataLength) {
payloadData = [self advanceData:payloadData dataOut:&auxillaryData length:auxillaryDataLength];
}
// Then comes the return value
size_t returnValueDataLength = payloadHeader.totalLength - payloadHeader.auxiliaryLength;
NSData *returnValueData = nil;
if (returnValueDataLength) {
[self advanceData:payloadData dataOut:&returnValueData length:returnValueDataLength];
}
// Then parse the payload items.
return [self parseReturnValueData:returnValueData auxillaryData:auxillaryData messageHeader:messageHeader error:error];
}
+ (ResponsePayload)parseReturnValueData:(NSData *)returnValueData auxillaryData:(NSData *)auxillaryData messageHeader:(DTXMessageHeader)messageHeader error:(NSError **)error
{
// Auxillary data comes first. This is typically only used in the handshake
id auxillaryValues = nil;
if (auxillaryData && auxillaryData.length > 0) {
auxillaryValues = [self objectArgumentsFromAuxillaryData:auxillaryData error:error];
if (!auxillaryValues) {
return InvalidResponsePayload;
}
}
// Then the return value of the RPC call. For some calls this will be the selector name.
id returnValue = nil;
if (returnValueData && returnValueData.length > 0) {
returnValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:self.supportedReturnSerializerValues fromData:returnValueData error:error];
if (!returnValue) {
return InvalidResponsePayload;
}
if ([returnValue isKindOfClass:NSError.class]) {
if (error) {
*error = returnValue;
}
return InvalidResponsePayload;
}
}
return (ResponsePayload) {
.success = YES,
.messageIdentifier = messageHeader.identifier,
.channelCode = messageHeader.channelCode,
.returnValue = returnValue,
.auxillaryValues = auxillaryValues,
};
}
#pragma mark Private Instance Methods
- (ResponsePayload)onChannelIdentifier:(NSString *)channelIdentifier performSelector:(NSString *)selector argumentsData:(nullable NSArray<NSData *> *)argumentsData error:(NSError **)error
{
NSNumber *channelCode = [self makeChannelWithIdentifier:channelIdentifier error:error];
if (!channelCode) {
return InvalidResponsePayload;
}
return [self onChannelCode:channelCode.unsignedIntValue performSelector:selector argumentsData:argumentsData error:error];
}
- (ResponsePayload)onChannelCode:(uint32)channelCode performSelector:(NSString *)selector argumentsData:(nullable NSArray<NSData *> *)argumentsData error:(NSError **)error
{
RequestPayload request = {
.selector = selector,
.argumentsData = argumentsData,
.messageIdentifier = [self nextMessageIdentifier],
.channelCode = channelCode,
.expectsReply = YES,
};
return [self requestSendAndReceive:request error:error];
}
- (NSNumber *)makeChannelWithIdentifier:(NSString *)identifier error:(NSError **)error
{
if (self.channels[identifier] == nil) {
return [[FBControlCoreError
describeFormat:@"Could not make a channel %@ as it is not one of %@", identifier, self.channels.allKeys]
fail:error];
}
int32_t channelIdentifier = [self nextChannelIdentifier];
RequestPayload request = {
.selector = @"_requestChannelWithCode:identifier:",
.argumentsData = @[
[FBInstrumentsClient argumentDataForInt32:channelIdentifier],
[FBInstrumentsClient argumentDataForArgument:identifier],
],
.messageIdentifier = [self nextMessageIdentifier],
.channelCode = 0,
.expectsReply = YES,
};
ResponsePayload response = [self requestSendAndReceive:request error:error];
if (response.success == NO) {
return nil;
}
return @(channelIdentifier);
}
- (ResponsePayload)requestSendAndReceive:(RequestPayload)request error:(NSError **)error
{
ResponsePayload response = [FBInstrumentsClient onConnection:self.connection requestSendAndReceive:request error:error];
if (response.success == NO) {
return InvalidResponsePayload;
}
self.lastMessageIdentifier = response.messageIdentifier;
return response;
}
- (uint32)nextMessageIdentifier
{
uint32 identifier = self.lastMessageIdentifier + 1;
self.lastMessageIdentifier = identifier;
return identifier;
}
- (int32_t)nextChannelIdentifier
{
int32_t identifier = self.lastChannelIdentifier + 1;
self.lastChannelIdentifier = identifier;
return identifier;
}
@end