diff --git a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx index 6d6ca2956..df4e7f816 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx @@ -9,6 +9,7 @@ import fs from 'fs-extra'; import child_process from 'child_process'; +import type {IOSDeviceParams} from 'flipper-common'; import {DeviceType} from 'flipper-common'; import {v1 as uuid} from 'uuid'; import path from 'path'; @@ -21,6 +22,15 @@ export const ERR_NO_IDB_OR_XCODE_AVAILABLE = export const ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB = 'Cannot provide logs from a physical device without idb.'; +// eslint-disable-next-line @typescript-eslint/naming-convention +type iOSSimulatorDevice = { + state: 'Booted' | 'Shutdown' | 'Shutting Down'; + availability?: string; + isAvailable?: 'YES' | 'NO' | true | false; + name: string; + udid: string; +}; + export interface IOSBridge { startLogListener: ( udid: string, @@ -115,12 +125,49 @@ export class SimctlBridge implements IOSBridge { ); } + async getActiveDevices(bootedOnly: boolean): Promise> { + return execFile('xcrun', [ + 'simctl', + ...getDeviceSetPath(), + 'list', + 'devices', + '--json', + ]) + .then(({stdout}) => JSON.parse(stdout!.toString()).devices) + .then((simulatorDevices: Array) => { + const simulators = Object.values(simulatorDevices).flat(); + return simulators + .filter( + (simulator) => + (!bootedOnly || simulator.state === 'Booted') && + isSimulatorAvailable(simulator), + ) + .map((simulator) => { + return { + ...simulator, + type: 'emulator', + } as IOSDeviceParams; + }); + }); + } + async launchSimulator(udid: string): Promise { await execFile('xcrun', ['simctl', ...getDeviceSetPath(), 'boot', udid]); await execFile('open', ['-a', 'simulator']); } } +function isSimulatorAvailable(simulator: iOSSimulatorDevice): boolean { + // For some users "availability" is set, for others it's "isAvailable" + // It's not clear which key is set, so we are checking both. + // We've also seen isAvailable return "YES" and true, depending on version. + return ( + simulator.availability === '(available)' || + simulator.isAvailable === 'YES' || + simulator.isAvailable === true + ); +} + async function isAvailable(idbPath: string): Promise { if (!idbPath) { return false; diff --git a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSDevice.node.tsx b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSDevice.node.tsx index ddef8270b..0d29e4c88 100644 --- a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSDevice.node.tsx +++ b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSDevice.node.tsx @@ -18,7 +18,17 @@ import { setFlipperServerConfig, } from '../../../FlipperServerConfig'; +let fakeSimctlBridge: any; +let hasCalledSimctlActiveDevices = false; + beforeEach(() => { + hasCalledSimctlActiveDevices = false; + fakeSimctlBridge = { + getActiveDevices: async (_bootedOnly: boolean) => { + hasCalledSimctlActiveDevices = true; + return []; + }, + }; setFlipperServerConfig(getRenderHostInstance().serverConfig); }); @@ -73,11 +83,13 @@ test('test getAllPromisesForQueryingDevices when xcode detected', () => { ); flipperServer.ios.iosBridge = {} as IOSBridge; (flipperServer.ios as any).idbConfig = getFlipperServerConfig().settings; + flipperServer.ios.simctlBridge = fakeSimctlBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices( true, false, ); expect(promises.length).toEqual(2); + expect(hasCalledSimctlActiveDevices).toEqual(true); }); test('test getAllPromisesForQueryingDevices when xcode is not detected', () => { @@ -87,11 +99,13 @@ test('test getAllPromisesForQueryingDevices when xcode is not detected', () => { ); flipperServer.ios.iosBridge = {} as IOSBridge; (flipperServer.ios as any).idbConfig = getFlipperServerConfig().settings; + flipperServer.ios.simctlBridge = fakeSimctlBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices( false, true, ); expect(promises.length).toEqual(1); + expect(hasCalledSimctlActiveDevices).toEqual(false); }); test('test getAllPromisesForQueryingDevices when xcode and idb are both unavailable', () => { @@ -101,11 +115,13 @@ test('test getAllPromisesForQueryingDevices when xcode and idb are both unavaila ); flipperServer.ios.iosBridge = {} as IOSBridge; (flipperServer.ios as any).idbConfig = getFlipperServerConfig().settings; + flipperServer.ios.simctlBridge = fakeSimctlBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices( false, false, ); expect(promises.length).toEqual(0); + expect(hasCalledSimctlActiveDevices).toEqual(false); }); test('test getAllPromisesForQueryingDevices when both idb and xcode are available', () => { @@ -115,9 +131,11 @@ test('test getAllPromisesForQueryingDevices when both idb and xcode are availabl ); flipperServer.ios.iosBridge = {} as IOSBridge; (flipperServer.ios as any).idbConfig = getFlipperServerConfig().settings; + flipperServer.ios.simctlBridge = fakeSimctlBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices( true, true, ); expect(promises.length).toEqual(2); + expect(hasCalledSimctlActiveDevices).toEqual(false); }); diff --git a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx index b10e1338f..099105641 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx @@ -11,14 +11,13 @@ import {ChildProcess} from 'child_process'; import type {IOSDeviceParams} from 'flipper-common'; import path from 'path'; import childProcess from 'child_process'; -import {exec, execFile} from 'promisify-child-process'; +import {exec} from 'promisify-child-process'; import iosUtil from './iOSContainerUtility'; import IOSDevice from './IOSDevice'; import { ERR_NO_IDB_OR_XCODE_AVAILABLE, IOSBridge, makeIOSBridge, - getDeviceSetPath, SimctlBridge, } from './IOSBridge'; import {FlipperServerImpl} from '../../FlipperServerImpl'; @@ -27,26 +26,6 @@ import {getFlipperServerConfig} from '../../FlipperServerConfig'; import {IdbConfig, setIdbConfig} from './idbConfig'; import {assertNotNull} from 'flipper-server-core/src/comms/Utilities'; -// eslint-disable-next-line @typescript-eslint/naming-convention -type iOSSimulatorDevice = { - state: 'Booted' | 'Shutdown' | 'Shutting Down'; - availability?: string; - isAvailable?: 'YES' | 'NO' | true | false; - name: string; - udid: string; -}; - -function isAvailable(simulator: iOSSimulatorDevice): boolean { - // For some users "availability" is set, for others it's "isAvailable" - // It's not clear which key is set, so we are checking both. - // We've also seen isAvailable return "YES" and true, depending on version. - return ( - simulator.availability === '(available)' || - simulator.isAvailable === 'YES' || - simulator.isAvailable === true - ); -} - export class IOSDeviceManager { private portForwarders: Array = []; private idbConfig?: IdbConfig; @@ -214,41 +193,18 @@ export class IOSDeviceManager { } getSimulators(bootedOnly: boolean): Promise> { - return execFile('xcrun', [ - 'simctl', - ...getDeviceSetPath(), - 'list', - 'devices', - '--json', - ]) - .then(({stdout}) => JSON.parse(stdout!.toString()).devices) - .then((simulatorDevices: Array) => { - const simulators = Object.values(simulatorDevices).flat(); - return simulators - .filter( - (simulator) => - (!bootedOnly || simulator.state === 'Booted') && - isAvailable(simulator), - ) - .map((simulator) => { - return { - ...simulator, - type: 'emulator', - } as IOSDeviceParams; - }); - }) - .catch((e: Error) => { - console.warn('Failed to query simulators:', e); - if (e.message.includes('Xcode license agreements')) { - this.flipperServer.emit('notification', { - type: 'error', - title: 'Xcode license requires approval', - description: - 'The Xcode license agreement has changed. You need to either open Xcode and agree to the terms or run `sudo xcodebuild -license` in a Terminal to allow simulators to work with Flipper.', - }); - } - return Promise.resolve([]); - }); + return this.simctlBridge.getActiveDevices(bootedOnly).catch((e: Error) => { + console.warn('Failed to query simulators:', e); + if (e.message.includes('Xcode license agreements')) { + this.flipperServer.emit('notification', { + type: 'error', + title: 'Xcode license requires approval', + description: + 'The Xcode license agreement has changed. You need to either open Xcode and agree to the terms or run `sudo xcodebuild -license` in a Terminal to allow simulators to work with Flipper.', + }); + } + return Promise.resolve([]); + }); } private queryDevicesForever() {