FBSimulatorControl/Commands/FBSimulatorLifecycleCommands.m (199 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 "FBSimulatorLifecycleCommands.h"
#import <CoreSimulator/SimDevice.h>
#import <AppKit/AppKit.h>
#import "FBCoreSimulatorNotifier.h"
#import "FBSimulator.h"
#import "FBSimulatorBootConfiguration.h"
#import "FBSimulatorBootStrategy.h"
#import "FBSimulatorBridge.h"
#import "FBSimulatorConfiguration+CoreSimulator.h"
#import "FBSimulatorConfiguration.h"
#import "FBSimulatorControl.h"
#import "FBSimulatorControlConfiguration.h"
#import "FBSimulatorError.h"
@interface FBSimulatorLifecycleCommands ()
@property (nonatomic, weak, readonly) FBSimulator *simulator;
@property (nonatomic, strong, readwrite, nullable) FBFramebuffer *framebuffer;
@property (nonatomic, strong, readwrite, nullable) FBSimulatorHID *hid;
@property (nonatomic, strong, readwrite, nullable) FBSimulatorBridge *bridge;
@end
@implementation FBSimulatorLifecycleCommands
#pragma mark Initializers
+ (instancetype)commandsWithTarget:(FBSimulator *)target
{
return [[self alloc] initWithSimulator:target];
}
- (instancetype)initWithSimulator:(FBSimulator *)simulator
{
self = [super init];
if (!self) {
return nil;
}
_simulator = simulator;
return self;
}
#pragma mark Boot/Shutdown
- (FBFuture<NSNull *> *)boot:(FBSimulatorBootConfiguration *)configuration
{
return [FBSimulatorBootStrategy boot:self.simulator withConfiguration:configuration];
}
#pragma mark FBPowerCommands
- (FBFuture<NSNull *> *)shutdown
{
return [[self.simulator.set shutdown:self.simulator] mapReplace:NSNull.null];
}
- (FBFuture<NSNull *> *)reboot
{
return [[self
shutdown]
onQueue:self.simulator.workQueue fmap:^(id _) {
return [self boot:FBSimulatorBootConfiguration.defaultConfiguration];
}];
}
#pragma mark Erase
- (FBFuture<NSNull *> *)erase
{
return [[self.simulator.set erase:self.simulator] mapReplace:NSNull.null];
}
#pragma mark States
- (FBFuture<NSNull *> *)resolveState:(FBiOSTargetState)state
{
return FBiOSTargetResolveState(self.simulator, state);
}
- (FBFuture<NSNull *> *)resolveLeavesState:(FBiOSTargetState)state
{
return [FBCoreSimulatorNotifier resolveLeavesState:state forSimDevice:self.simulator.device];
}
#pragma mark Focus
- (FBFuture<NSNull *> *)focus
{
// We cannot 'focus' a SimulatorApp for the non-default device set.
NSString *deviceSetPath = self.simulator.customDeviceSetPath;
if (deviceSetPath) {
return [[FBSimulatorError
describeFormat:@"Focusing on the Simulator App for a simulator in a custom device set (%@) is not supported", deviceSetPath]
failFuture];
}
// Find the running instances of SimulatorApp.
NSArray<NSRunningApplication *> *apps = NSWorkspace.sharedWorkspace.runningApplications;
NSPredicate *simulatorAppPredicate = [NSPredicate predicateWithBlock:^(NSRunningApplication *application, NSDictionary<NSString *,id> *__) {
return [application.bundleIdentifier isEqualToString:@"com.apple.iphonesimulator"];
}];
NSArray<NSRunningApplication *> *simulatorApps = [apps filteredArrayUsingPredicate:simulatorAppPredicate];
// If we have no SimulatorApp running then we can instead launch one in a focused state
if (simulatorApps.count == 0) {
NSError *error = nil;
NSRunningApplication *simulatorApp = [FBSimulatorLifecycleCommands launchSimulatorApplicationForDefaultDeviceSetWithError:&error];
if (!simulatorApp) {
return [FBFuture futureWithError:error];
}
return FBFuture.empty;
}
// Multiple apps, we don't know which to select.
if (simulatorApps.count > 1) {
return [[FBSimulatorError
describeFormat:@"More than one SimulatorApp %@ running, focus is ambiguous", [FBCollectionInformation oneLineDescriptionFromArray:simulatorApps]]
failFuture];
}
// Otherwise we have a single Simulator App to activate.
NSRunningApplication *simulatorApp = simulatorApps.firstObject;
if (![simulatorApp activateWithOptions:NSApplicationActivateIgnoringOtherApps]) {
return [[FBSimulatorError
describeFormat:@"Failed to focus %@", simulatorApp]
failFuture];
}
return FBFuture.empty;
}
+ (NSRunningApplication *)launchSimulatorApplicationForDefaultDeviceSetWithError:(NSError **)error
{
// Obtain the location of the SimulatorApp
FBBundleDescriptor *applicationBundle = FBXcodeConfiguration.simulatorApp;
NSURL *applicationURL = [NSURL fileURLWithPath:applicationBundle.path];
// We only want to ever connect to the default SimulatorApp, including re-activating it rather than creating a new instance.
NSError *innerError = nil;
NSRunningApplication *application = [NSWorkspace.sharedWorkspace
launchApplicationAtURL:applicationURL
options:NSWorkspaceLaunchDefault
configuration:@{}
error:&innerError];
if (!application) {
return [[[FBSimulatorError
describe:@"Failed to launch SimulatorApp"]
causedBy:innerError]
fail:error];
}
return application;
}
#pragma mark Connection
- (FBFuture<NSNull *> *)disconnectWithTimeout:(NSTimeInterval)timeout logger:(nullable id<FBControlCoreLogger>)logger
{
NSDate *date = NSDate.date;
return [[[self
terminateConnections]
timeout:timeout waitingFor:@"Simulator connections to teardown"]
onQueue:self.simulator.workQueue map:^(id _) {
[logger.debug logFormat:@"Simulator connections torn down in %f seconds", [NSDate.date timeIntervalSinceDate:date]];
return NSNull.null;
}];
}
- (FBFuture<NSNull *> *)terminateConnections
{
FBSimulatorHID *hid = self.hid;
FBSimulatorBridge *bridge = self.bridge;
return [[FBFuture
futureWithFutures:@[
(hid ? [hid disconnect] : FBFuture.empty),
(bridge ? [bridge disconnect] : FBFuture.empty),
]]
onQueue:self.simulator.workQueue chain:^(FBFuture *_) {
// Nullify
self.framebuffer = nil;
self.hid = nil;
self.bridge = nil;
return FBFuture.empty;
}];
}
#pragma mark Bridge
- (FBFuture<FBSimulatorBridge *> *)connectToBridge
{
if (self.bridge) {
return [FBFuture futureWithResult:self.bridge];
}
return [[FBSimulatorBridge
bridgeForSimulator:self.simulator]
onQueue:self.simulator.workQueue map:^(FBSimulatorBridge *bridge) {
self.bridge = bridge;
return bridge;
}];
}
#pragma mark Framebuffer
- (FBFuture<FBFramebuffer *> *)connectToFramebuffer
{
if (self.framebuffer) {
return [FBFuture futureWithResult:self.framebuffer];
}
FBSimulator *simulator = self.simulator;
return [FBFuture
onQueue:simulator.workQueue resolveValue:^(NSError **error) {
return [FBFramebuffer mainScreenSurfaceForSimulator:simulator logger:simulator.logger error:error];
}];
}
#pragma mark Bridge
- (FBFuture<FBSimulatorHID *> *)connectToHID
{
if (self.hid) {
return [FBFuture futureWithResult:self.hid];
}
return [[FBSimulatorHID
hidForSimulator:self.simulator]
onQueue:self.simulator.workQueue map:^(FBSimulatorHID *hid) {
self.hid = hid;
return hid;
}];
}
#pragma mark URLs
- (FBFuture<NSNull *> *)openURL:(NSURL *)url
{
NSParameterAssert(url);
NSError *error = nil;
if (![self.simulator.device openURL:url error:&error]) {
return [[[FBSimulatorError
describeFormat:@"Failed to open URL %@ on simulator %@", url, self.simulator]
causedBy:error]
failFuture];
}
return [FBFuture futureWithResult:[NSNull null]];
}
@end