FBSimulatorControl/Management/FBSimulatorBridge.m (316 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 "FBSimulatorBridge.h" #import <FBControlCore/FBControlCore.h> #import <CoreSimulator/SimDevice.h> #import <SimulatorBridge/SimulatorBridge-Protocol.h> #import "FBSimulator.h" #import "FBSimulatorError.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" static NSString *const KeyAXTraits = @"AXTraits"; static NSString *const KeyTraits = @"traits"; static NSString *const KeyType = @"type"; static NSTimeInterval BridgeReadyTimeout = 5.0; @interface FBSimulatorBridge () @property (nonatomic, strong, nullable, readwrite) NSProxy<SimulatorBridge> *bridge; @property (nonatomic, strong, nullable, readwrite) FBProcess *process; @property (nonatomic, strong, readonly) dispatch_queue_t workQueue; @property (nonatomic, strong, readonly) dispatch_queue_t asyncQueue; @end @implementation FBSimulatorBridge #pragma mark Initializers + (dispatch_queue_t)createBridgeQueue { return dispatch_queue_create("com.facebook.fbsimulatorcontrol.bridge", DISPATCH_QUEUE_SERIAL); } + (NSString *)simulatorBridgeLaunchPathWithError:(NSError **)error { FBBundleDescriptor *simulatorApp = FBXcodeConfiguration.simulatorApp; if (FBXcodeConfiguration.isXcode12_5OrGreater) { return [[FBControlCoreError describe:@"Some idb functionality is not yet available with Xcode 12.5, downgrade your Xcode version and try again."] fail:error]; } NSString *path = [simulatorApp.path stringByAppendingPathComponent:@"Contents/Resources/Platforms/iphoneos/usr/libexec/SimulatorBridge"]; FBBinaryDescriptor *binary = [FBBinaryDescriptor binaryWithPath:path error:error]; if (!binary) { return nil; } return binary.path; } + (FBFuture<id<SimulatorBridge>> *)bridgeForSimulator:(FBSimulator *)simulator { // Connect to the expected-to-be-running CoreSimulatorBridge running inside the Simulator. // This mimics the behaviour of Simulator.app, which just looks up the service then connects to the distant object over a Remote Object connection. dispatch_queue_t bridgeQueue = FBSimulatorBridge.createBridgeQueue; dispatch_queue_t asyncQueue = simulator.asyncQueue; return [[[self bridgeProcessForSimulator:simulator] onQueue:simulator.workQueue map:^(NSArray<id> *tuple) { NSCParameterAssert(tuple.count >= 1); NSProxy<SimulatorBridge> *bridge = tuple[0]; FBProcess *process = (tuple.count == 2) ? tuple[1] : nil; return [[FBSimulatorBridge alloc] initWithBridge:bridge process:process workQueue:bridgeQueue asyncQueue:asyncQueue]; }] onQueue:bridgeQueue fmap:^(FBSimulatorBridge *bridge) { [simulator.logger logFormat:@"Enabling Accessibility on the bridge %@", bridge]; return [[bridge enableAccessibility] mapReplace:bridge]; }]; } + (FBFuture<NSArray<id> *> *)bridgeProcessForSimulator:(FBSimulator *)simulator; { NSTimeInterval timeout = FBControlCoreGlobalConfiguration.fastTimeout; NSString *portName = [self portNameForSimulator:simulator]; return [[self bridgeForSimulator:simulator portName:portName] onQueue:simulator.workQueue chain:^(FBFuture *future) { // If the Bridge Could not be Constructed, spawn the SimulatorBridge Process // Re-Attempt to obtain the SimulatorBridge Object at the same time, with a timeout if (future.error) { return [FBFuture futureWithFutures:@[ [FBSimulatorBridge bridgeForSimulator:simulator portName:portName timeout:timeout], [FBSimulatorBridge bridgeOperationForSimulator:simulator portName:portName], ]]; } // Otherwise just return the original future wrapped in the array. [simulator.logger logFormat:@"SimulatorBridge Agent %@ is already running for %@", future.result, portName]; return [FBFuture futureWithFutures:@[future]]; }]; } static NSString *const SimulatorBridgePortSuffix = @"FBSimulatorControl"; + (NSString *)portNameForSimulator:(FBSimulator *)simulator { NSString *portName = @"com.apple.iphonesimulator.bridge"; if (!FBXcodeConfiguration.isXcode9OrGreater) { return portName; } return [portName stringByAppendingFormat:@".%@", SimulatorBridgePortSuffix]; } + (FBFuture<FBProcess * > *)bridgeOperationForSimulator:(FBSimulator *)simulator portName:(NSString *)portName { NSError *error = nil; NSString *bridgeLaunchPath = [self simulatorBridgeLaunchPathWithError:&error]; if (!bridgeLaunchPath) { return [FBFuture futureWithError:error]; } id<FBControlCoreLogger> logger = [simulator.logger withName:@"SimulatorBridge"]; id<FBNotifyingBuffer> buffer = FBDataBuffer.notifyingBuffer; FBProcessIO *processIO = [[FBProcessIO alloc] initWithStdIn:nil stdOut:[FBProcessOutput outputForDataConsumer:buffer] stdErr:[FBProcessOutput outputForLogger:logger]]; FBProcessSpawnConfiguration *config = [[FBProcessSpawnConfiguration alloc] initWithLaunchPath:bridgeLaunchPath arguments:@[portName] environment:@{} io:processIO mode:FBProcessSpawnModeDefault]; [logger logFormat:@"Launching SimulatorBridge agent for %@", portName]; return [[[simulator launchProcess:config] onQueue:simulator.asyncQueue fmap:^(FBProcess *process) { return [[[buffer consumeAndNotifyWhen:[@"READY" dataUsingEncoding:NSUTF8StringEncoding]] timeout:BridgeReadyTimeout waitingFor:@"The launched process %@ to specify 'READY' for %@", process, portName] mapReplace:process]; }] onQueue:simulator.asyncQueue doOnResolved:^(FBProcess *process) { [logger logFormat:@"Bridge process is launched %@. %@ is now ready", process, portName]; }]; } + (FBFuture<NSProxy<SimulatorBridge> *> *)bridgeForSimulator:(FBSimulator *)simulator portName:(NSString *)portName timeout:(NSTimeInterval)timeout { return [[[FBFuture onQueue:simulator.workQueue resolveUntil:^{ return [self bridgeForSimulator:simulator portName:portName]; }] timeout:timeout waitingFor:@"Bridge Port %@ to exist", portName] onQueue:simulator.asyncQueue doOnResolved:^(NSProxy<SimulatorBridge> *bridge) { [simulator.logger logFormat:@"SimulatorBridge Proxy %@ for %@ is now ready", bridge, portName]; }]; } + (FBFuture<NSProxy<SimulatorBridge> *> *)bridgeForSimulator:(FBSimulator *)simulator portName:(NSString *)portName { return [[FBSimulatorBridge bridgePortForSimulator:simulator portName:portName] onQueue:simulator.workQueue fmap:^(NSNumber *bridgePort) { // Convert the bridge port to a remote object. NSPort *bridgeMachPort = [NSMachPort portWithMachPort:bridgePort.unsignedIntValue]; NSConnection *bridgeConnection = [NSConnection connectionWithReceivePort:nil sendPort:bridgeMachPort]; NSDistantObject *bridgeDistantObject = [bridgeConnection rootProxy]; // Check that the Distant Object Responds to some known selector NSProxy<SimulatorBridge> *bridge = (NSProxy<SimulatorBridge> *) bridgeDistantObject; SEL knownSelector = @selector(setLocationScenarioWithPath:); if (![bridge respondsToSelector:knownSelector]) { return [[FBSimulatorError describeFormat:@"Distant Object '%@' for '%@' at isn't a SimulatorBridge as it doesn't respond to %@", portName, bridgeDistantObject, NSStringFromSelector(knownSelector)] failFuture]; } return [FBFuture futureWithResult:bridge]; }]; } + (FBFuture<NSNumber *> *)bridgePortForSimulator:(FBSimulator *)simulator portName:(NSString *)portName { NSError *error = nil; mach_port_t bridgePort = [simulator.device lookup:portName error:&error]; if (bridgePort == 0) { return [[[FBSimulatorError describeFormat:@"Could not lookup mach port for '%@'", portName] causedBy:error] failFuture]; } return [FBFuture futureWithResult:@(bridgePort)]; } - (instancetype)initWithBridge:(NSProxy<SimulatorBridge> *)bridge process:(FBProcess *)process workQueue:(dispatch_queue_t)workQueue asyncQueue:(dispatch_queue_t)asyncQueue { self = [super init]; if (!self) { return nil; } _bridge = bridge; _process = process; _workQueue = workQueue; _asyncQueue = asyncQueue; return self; } #pragma mark Lifecycle - (FBFuture<NSNull *> *)disconnect { return [FBFuture onQueue:self.workQueue resolve:^ FBFuture<NSNull *> * { // Close the connection with the SimulatorBridge and nullify NSDistantObject *bridge = (NSDistantObject *) self.bridge; self.bridge = nil; if (!bridge) { return FBFuture.empty; } [[bridge connectionForProxy] invalidate]; // Dispose of the process FBProcess *process = self.process; self.process = nil; if (!process) { return FBFuture.empty; } return [[process sendSignal:SIGTERM backingOffToKillWithTimeout:1 logger:nil] mapReplace:NSNull.null]; }]; } #pragma mark Interacting with the Simulator - (FBFuture<NSNull *> *)enableAccessibility { return [[self interactWithBridge] onQueue:self.workQueue fmap:^ FBFuture * _Nonnull (id<SimulatorBridge> bridge) { if ([bridge respondsToSelector:@selector(enableAccessibility)]) { [bridge performSelector:@selector(enableAccessibility)]; } if (![bridge respondsToSelector:@selector(accessibilityEnabled)]) { return FBFuture.empty; } NSNumber *enabled = [bridge performSelector:@selector(accessibilityEnabled)]; if (enabled.boolValue != YES) { return [[FBSimulatorError describeFormat:@"Could not enable accessibility for bridge '%@'", bridge] failFuture]; } return FBFuture.empty; }]; } - (FBFuture<NSArray<NSDictionary<NSString *, id> *> *> *)accessibilityElements { return [[[self interactWithBridge] onQueue:self.workQueue fmap:^(id<SimulatorBridge> bridge) { id elements = [bridge accessibilityElementsWithDisplayId:0]; if (!elements) { return [[FBSimulatorError describeFormat:@"No Elements returned from bridge"] failFuture]; } return [FBFuture futureWithResult:elements]; }] onQueue:self.asyncQueue map:^(id elements) { return [FBSimulatorBridge jsonSerializableAccessibility:elements]; }]; } - (FBFuture<NSDictionary<NSString *, id> *> *)accessibilityElementAtPoint:(NSPoint)point { return [[[self interactWithBridge] onQueue:self.workQueue fmap:^(id<SimulatorBridge>bridge) { id element = [bridge accessibilityElementForPoint:point.x andY:point.y displayId:0]; if (!element) { return [[FBSimulatorError describeFormat:@"No Elements at %f,%f returned from bridge", point.x, point.y] failFuture]; } return [FBFuture futureWithResult:element]; }] onQueue:self.asyncQueue map:^(id element) { return [FBSimulatorBridge jsonSerializableElement:element]; }]; } - (FBFuture<NSNull *> *)setLocationWithLatitude:(double)latitude longitude:(double)longitude { return [[self interactWithBridge] onQueue:self.workQueue fmap:^(id<SimulatorBridge>bridge) { [bridge setLocationWithLatitude:latitude andLongitude:longitude]; return FBFuture.empty; }]; } - (FBFuture<NSNull *> *)setHardwareKeyboardEnabled:(BOOL)enabled { return [[self interactWithBridge] onQueue:self.workQueue fmap:^(id<SimulatorBridge> bridge) { [bridge setHardwareKeyboardEnabled:enabled keyboardType:0]; return FBFuture.empty; }]; } #pragma mark Private - (FBFuture<id<SimulatorBridge>> *)interactWithBridge { id<SimulatorBridge> bridge = self.bridge; if (!bridge) { return [[FBSimulatorError describeFormat:@"Cannot interact with bridge as it has been destroyed"] failFuture]; } NSDistantObject *distantObject = (NSDistantObject *) bridge; if (!distantObject.connectionForProxy.isValid) { return [[FBSimulatorError describeFormat:@"Cannot interact with bridge as the connection is invalid"] failFuture]; } return [FBFuture futureWithResult:bridge]; } + (NSArray<NSDictionary<NSString *, id> *> *)jsonSerializableAccessibility:(NSArray *)data { NSMutableArray<NSDictionary<NSString *, id> *> *array = [NSMutableArray array]; for (NSDictionary<NSString *, id> *oldItem in data) { NSDictionary<NSString *, id> *item = [self jsonSerializableElement:oldItem]; [array addObject:[item copy]]; } return [array copy]; } + (NSDictionary<NSString *, id> *)jsonSerializableElement:(NSDictionary<NSString *, id> *)oldItem { NSMutableDictionary<NSString *, id> *item = [NSMutableDictionary dictionary]; for (NSString *key in oldItem.allKeys) { id value = oldItem[key]; if ([key isEqualToString:KeyAXTraits]) { uint64_t bitmask = [(NSNumber *)value unsignedIntegerValue]; item[KeyTraits] = AXExtractTraits(bitmask).allObjects; item[KeyType] = AXExtractTypeFromTraits(bitmask); } else if ([value isKindOfClass:NSString.class] || [value isKindOfClass:NSNumber.class]) { item[key] = oldItem[key]; } else if ([value isKindOfClass:NSValue.class]) { item[key] = NSStringFromRect([value rectValue]); } } return item; } #pragma mark NSObject - (NSString *)description { if (self.bridge) { return @"Simulator Bridge: Connected"; } return @"Simulator Bridge: Disconnected"; } @end #pragma GCC diagnostic pop