iOS get devices/targets/simulators cleanup

Summary: ^

Reviewed By: passy

Differential Revision: D48781211

fbshipit-source-id: 71133c07d15ca6a380d85e582d55cbdb192b5a19
This commit is contained in:
Lorenzo Blasa
2023-08-30 04:24:05 -07:00
committed by Facebook GitHub Bot
parent 0045f15e2a
commit 3e8f94ceda
9 changed files with 170 additions and 180 deletions

View File

@@ -7,6 +7,8 @@
* @format * @format
*/ */
import {DeviceType, OS} from './server-types';
export interface PluginDetails { export interface PluginDetails {
name: string; name: string;
specVersion: number; specVersion: number;
@@ -57,17 +59,6 @@ export interface SupportedApp {
readonly type?: DeviceType; readonly type?: DeviceType;
} }
export type OS =
| 'iOS'
| 'Android'
| 'Metro'
| 'Windows'
| 'MacOS'
| 'Browser'
| 'Linux';
export type DeviceType = 'emulator' | 'physical' | 'dummy';
export type PluginType = 'client' | 'device'; export type PluginType = 'client' | 'device';
export type DeviceSpec = 'KaiOS'; export type DeviceSpec = 'KaiOS';

View File

@@ -10,11 +10,9 @@
import {FlipperDoctor} from './doctor'; import {FlipperDoctor} from './doctor';
import { import {
DeviceSpec, DeviceSpec,
DeviceType,
DownloadablePluginDetails, DownloadablePluginDetails,
InstalledPluginDetails, InstalledPluginDetails,
MarketplacePluginDetails, MarketplacePluginDetails,
OS as PluginOS,
UpdatablePluginDetails, UpdatablePluginDetails,
} from './PluginDetails'; } from './PluginDetails';
import {ServerAddOnStartDetails} from './ServerAddOn'; import {ServerAddOnStartDetails} from './ServerAddOn';
@@ -39,7 +37,7 @@ export type FlipperServerState =
| 'error' | 'error'
| 'closed'; | 'closed';
export type DeviceOS = PluginOS; export type DeviceOS = OS;
export type DeviceDescription = { export type DeviceDescription = {
readonly os: DeviceOS; readonly os: DeviceOS;
@@ -172,12 +170,22 @@ export type FlipperServerEvents = {
'server-log': LoggerInfo; 'server-log': LoggerInfo;
}; };
export type IOSDeviceParams = { export type OS =
| 'iOS'
| 'Android'
| 'Metro'
| 'Windows'
| 'MacOS'
| 'Browser'
| 'Linux';
export type DeviceType = 'physical' | 'emulator' | 'dummy';
export type DeviceTarget = {
udid: string; udid: string;
type: DeviceType; type: DeviceType;
name: string; name: string;
osVersion?: string; osVersion?: string;
deviceTypeIdentifier?: string;
state?: string; state?: string;
}; };
@@ -298,7 +306,7 @@ export type FlipperServerCommands = {
'android-get-emulators': () => Promise<string[]>; 'android-get-emulators': () => Promise<string[]>;
'android-launch-emulator': (name: string, coldboot: boolean) => Promise<void>; 'android-launch-emulator': (name: string, coldboot: boolean) => Promise<void>;
'android-adb-kill': () => Promise<void>; 'android-adb-kill': () => Promise<void>;
'ios-get-simulators': (bootedOnly: boolean) => Promise<IOSDeviceParams[]>; 'ios-get-simulators': (bootedOnly: boolean) => Promise<DeviceTarget[]>;
'ios-launch-simulator': (udid: string) => Promise<void>; 'ios-launch-simulator': (udid: string) => Promise<void>;
'ios-idb-kill': () => Promise<void>; 'ios-idb-kill': () => Promise<void>;
'persist-settings': (settings: Settings) => Promise<void>; 'persist-settings': (settings: Settings) => Promise<void>;

View File

@@ -496,7 +496,7 @@ export class FlipperServerImpl implements FlipperServer {
}, },
'ios-launch-simulator': async (udid) => { 'ios-launch-simulator': async (udid) => {
assertNotNull(this.ios); assertNotNull(this.ios);
return this.ios.simctlBridge.launchSimulator(udid); return this.ios.launchSimulator(udid);
}, },
'ios-idb-kill': async () => { 'ios-idb-kill': async () => {
assertNotNull(this.ios); assertNotNull(this.ios);

View File

@@ -8,10 +8,14 @@
*/ */
import fs from 'fs-extra'; import fs from 'fs-extra';
import iosUtil from './iOSContainerUtility'; import iosUtil, {
getDeviceSetPath,
isIdbAvailable,
queryTargetsWithXcode,
} from './iOSContainerUtility';
import child_process from 'child_process'; import child_process from 'child_process';
import type {IOSDeviceParams} from 'flipper-common'; import type {DeviceTarget} from 'flipper-common';
import {DeviceType, uuid} from 'flipper-common'; import {DeviceType, uuid} from 'flipper-common';
import path from 'path'; import path from 'path';
import {ChildProcessPromise, exec, execFile} from 'promisify-child-process'; import {ChildProcessPromise, exec, execFile} from 'promisify-child-process';
@@ -42,16 +46,6 @@ interface IOSInstalledAppDescriptor {
debuggableStatus: boolean; debuggableStatus: boolean;
} }
function getOSVersionFromXCRunOutput(s: string): string | undefined {
// E.g. 'com.apple.CoreSimulator.SimRuntime.iOS-16-1'
const match = s.match(
/com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)/,
);
if (match) {
return `${match[1]}.${match[2]}`;
}
}
export interface IOSBridge { export interface IOSBridge {
startLogListener: ( startLogListener: (
udid: string, udid: string,
@@ -63,7 +57,7 @@ export interface IOSBridge {
serial: string, serial: string,
outputFile: string, outputFile: string,
) => child_process.ChildProcess; ) => child_process.ChildProcess;
getActiveDevices: (bootedOnly: boolean) => Promise<Array<IOSDeviceParams>>; getActiveDevices: (bootedOnly: boolean) => Promise<Array<DeviceTarget>>;
installApp: ( installApp: (
serial: string, serial: string,
ipaPath: string, ipaPath: string,
@@ -77,6 +71,7 @@ export interface IOSBridge {
bundleId: string, bundleId: string,
dst: string, dst: string,
) => Promise<void>; ) => Promise<void>;
launchSimulator(udid: string): Promise<any>;
} }
export class IDBBridge implements IOSBridge { export class IDBBridge implements IOSBridge {
@@ -84,6 +79,10 @@ export class IDBBridge implements IOSBridge {
private idbPath: string, private idbPath: string,
private enablePhysicalDevices: boolean, private enablePhysicalDevices: boolean,
) {} ) {}
async launchSimulator(udid: string): Promise<any> {
await this._execIdb(`boot --udid ${udid}`);
await execFile('open', ['-a', 'simulator']);
}
async getInstalledApps(serial: string): Promise<IOSInstalledAppDescriptor[]> { async getInstalledApps(serial: string): Promise<IOSInstalledAppDescriptor[]> {
const {stdout} = await this._execIdb(`list-apps --udid ${serial}`); const {stdout} = await this._execIdb(`list-apps --udid ${serial}`);
@@ -150,9 +149,9 @@ export class IDBBridge implements IOSBridge {
await this._execIdb(`install ${ipaPath} --udid ${serial}`); await this._execIdb(`install ${ipaPath} --udid ${serial}`);
} }
async getActiveDevices(_bootedOnly: boolean): Promise<IOSDeviceParams[]> { async getActiveDevices(bootedOnly: boolean): Promise<DeviceTarget[]> {
return iosUtil return iosUtil
.targets(this.idbPath, this.enablePhysicalDevices) .targets(this.idbPath, this.enablePhysicalDevices, bootedOnly)
.catch((e) => { .catch((e) => {
console.warn('Failed to get active iOS devices:', e.message); console.warn('Failed to get active iOS devices:', e.message);
return []; return [];
@@ -285,31 +284,10 @@ export class SimctlBridge implements IOSBridge {
); );
} }
async getActiveDevices(bootedOnly: boolean): Promise<Array<IOSDeviceParams>> { async getActiveDevices(bootedOnly: boolean): Promise<Array<DeviceTarget>> {
return execFile('xcrun', [ const devices = await queryTargetsWithXcode();
'simctl', return devices.filter(
...getDeviceSetPath(), (target) => !bootedOnly || (bootedOnly && target.state === 'booted'),
'list',
'devices',
'--json',
])
.then(({stdout}) => JSON.parse(stdout!.toString()).devices)
.then((simulatorDevices: {[key: string]: Array<iOSSimulatorDevice>}) =>
Object.keys(simulatorDevices).flatMap((key: string) =>
simulatorDevices[key]
.filter(
(simulator: iOSSimulatorDevice) =>
(!bootedOnly || simulator.state === 'Booted') &&
isSimulatorAvailable(simulator),
)
.map((simulator: iOSSimulatorDevice) => {
return {
...simulator,
type: 'emulator',
osVersion: getOSVersionFromXCRunOutput(key),
} as IOSDeviceParams;
}),
),
); );
} }
@@ -319,27 +297,6 @@ export class SimctlBridge implements IOSBridge {
} }
} }
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 isIdbAvailable(idbPath: string): Promise<boolean> {
if (!idbPath) {
return false;
}
return fs.promises
.access(idbPath, fs.constants.X_OK)
.then((_) => true)
.catch((_) => false);
}
function getLogExtraArgs(deviceType: DeviceType) { function getLogExtraArgs(deviceType: DeviceType) {
if (deviceType === 'physical') { if (deviceType === 'physical') {
return [ return [
@@ -364,7 +321,6 @@ function makeTempScreenshotFilePath() {
} }
async function unzip(filePath: string, destination: string): Promise<void> { async function unzip(filePath: string, destination: string): Promise<void> {
// TODO: probably shouldn't involve shelling out.
await exec(`unzip -qq -o ${filePath} -d ${destination}`); await exec(`unzip -qq -o ${filePath} -d ${destination}`);
if (!(await fs.pathExists(path.join(destination, 'Payload')))) { if (!(await fs.pathExists(path.join(destination, 'Payload')))) {
throw new Error( throw new Error(
@@ -379,12 +335,6 @@ async function readScreenshotIntoBuffer(imagePath: string): Promise<Buffer> {
return buffer; return buffer;
} }
export function getDeviceSetPath() {
return process.env.DEVICE_SET_PATH
? ['--set', process.env.DEVICE_SET_PATH]
: [];
}
export async function makeIOSBridge( export async function makeIOSBridge(
idbPath: string, idbPath: string,
isXcodeDetected: boolean, isXcodeDetected: boolean,

View File

@@ -14,12 +14,12 @@ import {
getFlipperServerConfig, getFlipperServerConfig,
setFlipperServerConfig, setFlipperServerConfig,
} from '../../../FlipperServerConfig'; } from '../../../FlipperServerConfig';
import {IOSDeviceParams} from 'flipper-common'; import {DeviceTarget} from 'flipper-common';
let fakeSimctlBridge: any; let fakeSimctlBridge: any;
let fakeIDBBridge: any; let fakeIDBBridge: any;
let fakeFlipperServer: any; let fakeFlipperServer: any;
const fakeDevices: IOSDeviceParams[] = [ const fakeDevices: DeviceTarget[] = [
{ {
udid: 'luke', udid: 'luke',
type: 'emulator', type: 'emulator',
@@ -122,7 +122,7 @@ test('test queryDevices when simctl used', async () => {
fakeFlipperServer, fakeFlipperServer,
getFlipperServerConfig().settings, getFlipperServerConfig().settings,
); );
ios.simctlBridge = fakeSimctlBridge; ios.ctlBridge = fakeSimctlBridge;
await ios.queryDevices(fakeSimctlBridge); await ios.queryDevices(fakeSimctlBridge);
@@ -145,7 +145,7 @@ test('test queryDevices when idb used', async () => {
fakeFlipperServer, fakeFlipperServer,
getFlipperServerConfig().settings, getFlipperServerConfig().settings,
); );
ios.simctlBridge = fakeSimctlBridge; ios.ctlBridge = fakeSimctlBridge;
await ios.queryDevices(fakeIDBBridge); await ios.queryDevices(fakeIDBBridge);

View File

@@ -48,6 +48,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
const targets = await iosUtil.targets( const targets = await iosUtil.targets(
this.idbConfig.idbPath, this.idbConfig.idbPath,
this.idbConfig.enablePhysicalIOS, this.idbConfig.enablePhysicalIOS,
true,
clientQuery, clientQuery,
); );
if (targets.length === 0) { if (targets.length === 0) {

View File

@@ -8,11 +8,10 @@
*/ */
import {Mutex} from 'async-mutex'; import {Mutex} from 'async-mutex';
import {exec as unsafeExec, Output} from 'promisify-child-process'; import {exec as unsafeExec, Output, execFile} from 'promisify-child-process';
import {reportPlatformFailures} from 'flipper-common'; import {DeviceTarget, DeviceType, reportPlatformFailures} from 'flipper-common';
import {promises, constants} from 'fs'; import {promises, constants} from 'fs';
import memoize from 'lodash.memoize'; import memoize from 'lodash.memoize';
import {notNull} from '../../utils/typeUtils';
import {promisify} from 'util'; import {promisify} from 'util';
import child_process from 'child_process'; import child_process from 'child_process';
import fs from 'fs-extra'; import fs from 'fs-extra';
@@ -25,6 +24,25 @@ export type IdbConfig = {
enablePhysicalIOS: boolean; enablePhysicalIOS: boolean;
}; };
export type IdbTarget = {
udid: string;
type: string;
name: string;
os_version: string;
architecture: string;
state?: string;
target_type?: string | DeviceType;
};
export type XcodeTarget = {
state: 'Booted' | 'Shutdown' | 'Shutting Down';
availability?: string;
isAvailable?: 'YES' | 'NO' | true | false;
name: string;
osVersion?: string;
udid: string;
};
// Use debug to get helpful logs when idb fails // Use debug to get helpful logs when idb fails
const IDB_LOG_LEVEL = 'DEBUG'; const IDB_LOG_LEVEL = 'DEBUG';
const LOG_TAG = 'iOSContainerUtility'; const LOG_TAG = 'iOSContainerUtility';
@@ -32,30 +50,11 @@ const CMD_RECORD_THROTTLE_COUNT = 10;
const mutex = new Mutex(); const mutex = new Mutex();
type IdbTarget = {
name: string;
udid: string;
state: 'Booted' | 'Shutdown';
type: string | DeviceType;
target_type?: string | DeviceType;
os_version: string;
architecture: string;
};
export type DeviceType = 'physical' | 'emulator';
export type DeviceTarget = {
udid: string;
type: DeviceType;
name: string;
osVersion?: string;
};
let idbDeviceListing = 0; let idbDeviceListing = 0;
let idbCompanionDeviceListing = 0; let idbCompanionDeviceListing = 0;
let xcodeDeviceListing = 0; let xcodeDeviceListing = 0;
async function isAvailable(idbPath: string): Promise<boolean> { export async function isIdbAvailable(idbPath: string): Promise<boolean> {
if (!idbPath) { if (!idbPath) {
return false; return false;
} }
@@ -74,16 +73,50 @@ async function safeExec(
return await unsafeExec(command).finally(release); return await unsafeExec(command).finally(release);
} }
async function queryTargetsWithXcode( export function getDeviceSetPath() {
context: any, return process.env.DEVICE_SET_PATH
? ['--set', process.env.DEVICE_SET_PATH]
: [];
}
export function isSimulatorAvailable(simulator: XcodeTarget): 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
);
}
function getOSVersionFromXCRunOutput(s: string): string | undefined {
// E.g. 'com.apple.CoreSimulator.SimRuntime.iOS-16-1'
const match = s.match(
/com\.apple\.CoreSimulator\.SimRuntime\.iOS-(\d+)-(\d+)/,
);
if (match) {
return `${match[1]}.${match[2]}`;
}
}
export async function queryTargetsWithXcode(
context?: any,
): Promise<Array<DeviceTarget>> { ): Promise<Array<DeviceTarget>> {
const cmd = 'xcrun xctrace list devices'; const cmd = 'xcrun simctl list devices --json';
const description = 'Query available devices with Xcode'; const description = 'Query available devices with Xcode';
const troubleshoot = `Xcode command line tools are not installed. const troubleshoot = `Xcode command line tools are not installed.
Run 'xcode-select --install' from terminal.`; Run 'xcode-select --install' from terminal.`;
try { try {
const {stdout} = await safeExec(cmd); const {stdout} = await execFile('xcrun', [
'simctl',
...getDeviceSetPath(),
'list',
'devices',
'--json',
]);
if (!stdout) { if (!stdout) {
recorder.event('cmd', { recorder.event('cmd', {
cmd, cmd,
@@ -105,17 +138,22 @@ async function queryTargetsWithXcode(
}); });
} }
return stdout const devices = JSON.parse(stdout.toString()).devices as {
.toString() [key: string]: Array<XcodeTarget>;
.split('\n') };
.map((line) => line.trim())
.filter(Boolean) return Object.keys(devices).flatMap((key: string) =>
.map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line)) devices[key]
.filter(notNull) .filter((simulator: XcodeTarget) => isSimulatorAvailable(simulator))
.filter(([_match, _name, _udid, isSim]) => !isSim) .map((simulator: XcodeTarget) => {
.map<DeviceTarget>(([_match, name, udid]) => { return {
return {udid, type: 'physical', name}; ...simulator,
}); type: 'emulator',
state: simulator.state.toLowerCase(),
osVersion: getOSVersionFromXCRunOutput(key),
} as DeviceTarget;
}),
);
} catch (e) { } catch (e) {
recorder.event('cmd', { recorder.event('cmd', {
cmd, cmd,
@@ -176,7 +214,7 @@ async function queryTargetsWithIdb(
} }
} }
async function queryTargetsWithIdbCompanion( async function _queryTargetsWithIdbCompanion(
idbCompanionPath: string, idbCompanionPath: string,
isPhysicalDeviceEnabled: boolean, isPhysicalDeviceEnabled: boolean,
context: any, context: any,
@@ -187,7 +225,7 @@ async function queryTargetsWithIdbCompanion(
const troubleshoot = `Unable to locate idb_companion in '${idbCompanionPath}'. const troubleshoot = `Unable to locate idb_companion in '${idbCompanionPath}'.
Try running sudo yum install -y fb-idb`; Try running sudo yum install -y fb-idb`;
if (await isAvailable(idbCompanionPath)) { if (await isIdbAvailable(idbCompanionPath)) {
try { try {
const {stdout} = await safeExec(cmd); const {stdout} = await safeExec(cmd);
if (!stdout) { if (!stdout) {
@@ -244,9 +282,6 @@ async function queryTargetsWithIdbCompanion(
function parseIdbTarget(line: string): DeviceTarget | undefined { function parseIdbTarget(line: string): DeviceTarget | undefined {
const parsed: IdbTarget = JSON.parse(line); const parsed: IdbTarget = JSON.parse(line);
if (parsed.state.toLocaleLowerCase() !== 'booted') {
return;
}
return { return {
udid: parsed.udid, udid: parsed.udid,
type: type:
@@ -255,6 +290,7 @@ function parseIdbTarget(line: string): DeviceTarget | undefined {
: ('physical' as DeviceType), : ('physical' as DeviceType),
name: parsed.name, name: parsed.name,
osVersion: parsed.os_version, osVersion: parsed.os_version,
state: parsed.state?.toLocaleLowerCase(),
}; };
} }
@@ -331,45 +367,36 @@ async function idbDescribeTarget(
async function targets( async function targets(
idbPath: string, idbPath: string,
isPhysicalDeviceEnabled: boolean, isPhysicalDeviceEnabled: boolean,
bootedOnly: boolean = false,
context?: any, context?: any,
): Promise<Array<DeviceTarget>> { ): Promise<Array<DeviceTarget>> {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
return []; return [];
} }
const bootedFilter = (targets: DeviceTarget[] | undefined) => {
return targets
? targets.filter(
(target) => !bootedOnly || (bootedOnly && target.state === 'booted'),
)
: [];
};
// If companion is started by some external process and its path // If companion is started by some external process and its path
// is provided to Flipper via IDB_COMPANION environment variable, // is provided to Flipper via IDB_COMPANION environment variable,
// use that instead and do not query other devices. // use that instead and do not query other devices.
// See stack of D36315576 for details // See stack of D36315576 for details
if (process.env.IDB_COMPANION) { if (process.env.IDB_COMPANION) {
const target = await idbDescribeTarget(idbPath, context); const target = await idbDescribeTarget(idbPath, context);
return target ? [target] : []; return bootedFilter(target ? [target] : []);
} }
const isXcodeInstalled = await isXcodeDetected(); if (await memoize(isIdbAvailable)(idbPath)) {
if (!isXcodeInstalled) { const targets = await queryTargetsWithIdb(idbPath, context);
if (!isPhysicalDeviceEnabled) { return bootedFilter(targets);
recorder.rawError(
'You are trying to connect a physical device. Please enable the toggle "Enable physical iOS device" from the setting screen.',
);
}
const idbCompanionPath = path.dirname(idbPath) + '/idb_companion';
return queryTargetsWithIdbCompanion(
idbCompanionPath,
isPhysicalDeviceEnabled,
context,
);
}
// Not all users have idb installed because you can still use
// Flipper with Simulators without it.
// But idb is MUCH more CPU efficient than xcrun, so
// when installed, use it. This still holds true
// with the move from instruments to xcrun.
if (await memoize(isAvailable)(idbPath)) {
return await queryTargetsWithIdb(idbPath, context);
} else { } else {
return queryTargetsWithXcode(context); const targets = await queryTargetsWithXcode(context);
return bootedFilter(targets);
} }
} }
@@ -459,7 +486,7 @@ async function pull(
} }
async function checkIdbIsInstalled(idbPath: string): Promise<void> { async function checkIdbIsInstalled(idbPath: string): Promise<void> {
const isInstalled = await isAvailable(idbPath); const isInstalled = await isIdbAvailable(idbPath);
if (!isInstalled) { if (!isInstalled) {
throw new Error( throw new Error(
`idb is required to use iOS devices. Install it with instructions `idb is required to use iOS devices. Install it with instructions

View File

@@ -8,7 +8,7 @@
*/ */
import {ChildProcess} from 'child_process'; import {ChildProcess} from 'child_process';
import type {IOSDeviceParams} from 'flipper-common'; import type {DeviceTarget} from 'flipper-common';
import path from 'path'; import path from 'path';
import childProcess from 'child_process'; import childProcess from 'child_process';
import {exec} from 'promisify-child-process'; import {exec} from 'promisify-child-process';
@@ -18,7 +18,6 @@ import {
ERR_NO_IDB_OR_XCODE_AVAILABLE, ERR_NO_IDB_OR_XCODE_AVAILABLE,
IOSBridge, IOSBridge,
makeIOSBridge, makeIOSBridge,
SimctlBridge,
} from './IOSBridge'; } from './IOSBridge';
import {FlipperServerImpl} from '../../FlipperServerImpl'; import {FlipperServerImpl} from '../../FlipperServerImpl';
import {getFlipperServerConfig} from '../../FlipperServerConfig'; import {getFlipperServerConfig} from '../../FlipperServerConfig';
@@ -34,7 +33,7 @@ export class IOSDeviceManager {
'MacOS', 'MacOS',
'PortForwardingMacApp', 'PortForwardingMacApp',
); );
simctlBridge: SimctlBridge = new SimctlBridge(); ctlBridge: IOSBridge | undefined;
readonly certificateProvider: iOSCertificateProvider; readonly certificateProvider: iOSCertificateProvider;
@@ -102,7 +101,7 @@ export class IOSDeviceManager {
return this.processDevices(bridge, devices); return this.processDevices(bridge, devices);
} }
private processDevices(bridge: IOSBridge, activeDevices: IOSDeviceParams[]) { private processDevices(bridge: IOSBridge, activeDevices: DeviceTarget[]) {
const currentDeviceIDs = new Set( const currentDeviceIDs = new Set(
this.flipperServer this.flipperServer
.getDevices() .getDevices()
@@ -134,9 +133,23 @@ export class IOSDeviceManager {
}); });
} }
async getBridge(): Promise<IOSBridge> {
if (this.ctlBridge !== undefined) {
return this.ctlBridge;
}
const isDetected = await iosUtil.isXcodeDetected();
this.ctlBridge = await makeIOSBridge(
this.idbConfig.idbPath,
isDetected,
this.idbConfig.enablePhysicalIOS,
);
return this.ctlBridge;
}
public async watchIOSDevices() { public async watchIOSDevices() {
try { try {
const isDetected = await iosUtil.isXcodeDetected();
if (this.idbConfig.enablePhysicalIOS) { if (this.idbConfig.enablePhysicalIOS) {
this.startDevicePortForwarders(); this.startDevicePortForwarders();
} }
@@ -144,11 +157,7 @@ export class IOSDeviceManager {
// Check for version mismatch now for immediate error handling. // Check for version mismatch now for immediate error handling.
await this.checkXcodeVersionMismatch(); await this.checkXcodeVersionMismatch();
// Awaiting the promise here to trigger immediate error handling. // Awaiting the promise here to trigger immediate error handling.
const bridge = await makeIOSBridge( const bridge = await this.getBridge();
this.idbConfig.idbPath,
isDetected,
this.idbConfig.enablePhysicalIOS,
);
await this.queryDevicesForever(bridge); await this.queryDevicesForever(bridge);
} catch (err) { } catch (err) {
// This case is expected if both Xcode and idb are missing. // This case is expected if both Xcode and idb are missing.
@@ -166,9 +175,10 @@ export class IOSDeviceManager {
} }
} }
async getSimulators(bootedOnly: boolean): Promise<Array<IOSDeviceParams>> { async getSimulators(bootedOnly: boolean): Promise<Array<DeviceTarget>> {
try { try {
return await this.simctlBridge.getActiveDevices(bootedOnly); const bridge = await this.getBridge();
return await bridge.getActiveDevices(bootedOnly);
} catch (e) { } catch (e) {
console.warn('Failed to query simulators:', e); console.warn('Failed to query simulators:', e);
if (e.message.includes('Xcode license agreements')) { if (e.message.includes('Xcode license agreements')) {
@@ -183,6 +193,15 @@ export class IOSDeviceManager {
} }
} }
async launchSimulator(udid: string) {
try {
const bridge = await this.getBridge();
await bridge.launchSimulator(udid);
} catch (e) {
console.warn('Failed to launch simulator:', e);
}
}
private async queryDevicesForever(bridge: IOSBridge) { private async queryDevicesForever(bridge: IOSBridge) {
try { try {
await this.queryDevices(bridge); await this.queryDevices(bridge);

View File

@@ -26,7 +26,7 @@ import {
theme, theme,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import {IOSDeviceParams} from 'flipper-common'; import {DeviceTarget} from 'flipper-common';
import {getRenderHostInstance} from 'flipper-frontend-core'; import {getRenderHostInstance} from 'flipper-frontend-core';
import SettingsSheet from '../../chrome/SettingsSheet'; import SettingsSheet from '../../chrome/SettingsSheet';
import {Link} from '../../ui'; import {Link} from '../../ui';
@@ -88,7 +88,7 @@ export const LaunchEmulatorDialog = withTrackingScope(
(state) => state.settingsState.enableAndroid, (state) => state.settingsState.enableAndroid,
); );
const [iosEmulators, setIosEmulators] = useState<IOSDeviceParams[]>([]); const [iosEmulators, setIosEmulators] = useState<DeviceTarget[]>([]);
const [androidEmulators, setAndroidEmulators] = useState<string[]>([]); const [androidEmulators, setAndroidEmulators] = useState<string[]>([]);
const [waitingForIos, setWaitingForIos] = useState(iosEnabled); const [waitingForIos, setWaitingForIos] = useState(iosEnabled);
const [waitingForAndroid, setWaitingForAndroid] = useState(androidEnabled); const [waitingForAndroid, setWaitingForAndroid] = useState(androidEnabled);
@@ -113,13 +113,7 @@ export const LaunchEmulatorDialog = withTrackingScope(
.flipperServer.exec('ios-get-simulators', false) .flipperServer.exec('ios-get-simulators', false)
.then((emulators) => { .then((emulators) => {
setWaitingForIos(false); setWaitingForIos(false);
setIosEmulators( setIosEmulators(emulators);
emulators.filter(
(device) =>
device.state === 'Shutdown' &&
device.deviceTypeIdentifier?.match(/iPhone|iPad/i),
),
);
}) })
.catch((e) => { .catch((e) => {
console.warn('Failed to find simulators', e); console.warn('Failed to find simulators', e);