iOS get devices/targets/simulators cleanup
Summary: ^ Reviewed By: passy Differential Revision: D48781211 fbshipit-source-id: 71133c07d15ca6a380d85e582d55cbdb192b5a19
This commit is contained in:
committed by
Facebook GitHub Bot
parent
0045f15e2a
commit
3e8f94ceda
@@ -8,10 +8,14 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import iosUtil from './iOSContainerUtility';
|
||||
import iosUtil, {
|
||||
getDeviceSetPath,
|
||||
isIdbAvailable,
|
||||
queryTargetsWithXcode,
|
||||
} from './iOSContainerUtility';
|
||||
|
||||
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 path from 'path';
|
||||
import {ChildProcessPromise, exec, execFile} from 'promisify-child-process';
|
||||
@@ -42,16 +46,6 @@ interface IOSInstalledAppDescriptor {
|
||||
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 {
|
||||
startLogListener: (
|
||||
udid: string,
|
||||
@@ -63,7 +57,7 @@ export interface IOSBridge {
|
||||
serial: string,
|
||||
outputFile: string,
|
||||
) => child_process.ChildProcess;
|
||||
getActiveDevices: (bootedOnly: boolean) => Promise<Array<IOSDeviceParams>>;
|
||||
getActiveDevices: (bootedOnly: boolean) => Promise<Array<DeviceTarget>>;
|
||||
installApp: (
|
||||
serial: string,
|
||||
ipaPath: string,
|
||||
@@ -77,6 +71,7 @@ export interface IOSBridge {
|
||||
bundleId: string,
|
||||
dst: string,
|
||||
) => Promise<void>;
|
||||
launchSimulator(udid: string): Promise<any>;
|
||||
}
|
||||
|
||||
export class IDBBridge implements IOSBridge {
|
||||
@@ -84,6 +79,10 @@ export class IDBBridge implements IOSBridge {
|
||||
private idbPath: string,
|
||||
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[]> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
async getActiveDevices(_bootedOnly: boolean): Promise<IOSDeviceParams[]> {
|
||||
async getActiveDevices(bootedOnly: boolean): Promise<DeviceTarget[]> {
|
||||
return iosUtil
|
||||
.targets(this.idbPath, this.enablePhysicalDevices)
|
||||
.targets(this.idbPath, this.enablePhysicalDevices, bootedOnly)
|
||||
.catch((e) => {
|
||||
console.warn('Failed to get active iOS devices:', e.message);
|
||||
return [];
|
||||
@@ -285,32 +284,11 @@ export class SimctlBridge implements IOSBridge {
|
||||
);
|
||||
}
|
||||
|
||||
async getActiveDevices(bootedOnly: boolean): Promise<Array<IOSDeviceParams>> {
|
||||
return execFile('xcrun', [
|
||||
'simctl',
|
||||
...getDeviceSetPath(),
|
||||
'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;
|
||||
}),
|
||||
),
|
||||
);
|
||||
async getActiveDevices(bootedOnly: boolean): Promise<Array<DeviceTarget>> {
|
||||
const devices = await queryTargetsWithXcode();
|
||||
return devices.filter(
|
||||
(target) => !bootedOnly || (bootedOnly && target.state === 'booted'),
|
||||
);
|
||||
}
|
||||
|
||||
async launchSimulator(udid: string): Promise<any> {
|
||||
@@ -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) {
|
||||
if (deviceType === 'physical') {
|
||||
return [
|
||||
@@ -364,7 +321,6 @@ function makeTempScreenshotFilePath() {
|
||||
}
|
||||
|
||||
async function unzip(filePath: string, destination: string): Promise<void> {
|
||||
// TODO: probably shouldn't involve shelling out.
|
||||
await exec(`unzip -qq -o ${filePath} -d ${destination}`);
|
||||
if (!(await fs.pathExists(path.join(destination, 'Payload')))) {
|
||||
throw new Error(
|
||||
@@ -379,12 +335,6 @@ async function readScreenshotIntoBuffer(imagePath: string): Promise<Buffer> {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function getDeviceSetPath() {
|
||||
return process.env.DEVICE_SET_PATH
|
||||
? ['--set', process.env.DEVICE_SET_PATH]
|
||||
: [];
|
||||
}
|
||||
|
||||
export async function makeIOSBridge(
|
||||
idbPath: string,
|
||||
isXcodeDetected: boolean,
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
getFlipperServerConfig,
|
||||
setFlipperServerConfig,
|
||||
} from '../../../FlipperServerConfig';
|
||||
import {IOSDeviceParams} from 'flipper-common';
|
||||
import {DeviceTarget} from 'flipper-common';
|
||||
|
||||
let fakeSimctlBridge: any;
|
||||
let fakeIDBBridge: any;
|
||||
let fakeFlipperServer: any;
|
||||
const fakeDevices: IOSDeviceParams[] = [
|
||||
const fakeDevices: DeviceTarget[] = [
|
||||
{
|
||||
udid: 'luke',
|
||||
type: 'emulator',
|
||||
@@ -122,7 +122,7 @@ test('test queryDevices when simctl used', async () => {
|
||||
fakeFlipperServer,
|
||||
getFlipperServerConfig().settings,
|
||||
);
|
||||
ios.simctlBridge = fakeSimctlBridge;
|
||||
ios.ctlBridge = fakeSimctlBridge;
|
||||
|
||||
await ios.queryDevices(fakeSimctlBridge);
|
||||
|
||||
@@ -145,7 +145,7 @@ test('test queryDevices when idb used', async () => {
|
||||
fakeFlipperServer,
|
||||
getFlipperServerConfig().settings,
|
||||
);
|
||||
ios.simctlBridge = fakeSimctlBridge;
|
||||
ios.ctlBridge = fakeSimctlBridge;
|
||||
|
||||
await ios.queryDevices(fakeIDBBridge);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
const targets = await iosUtil.targets(
|
||||
this.idbConfig.idbPath,
|
||||
this.idbConfig.enablePhysicalIOS,
|
||||
true,
|
||||
clientQuery,
|
||||
);
|
||||
if (targets.length === 0) {
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
*/
|
||||
|
||||
import {Mutex} from 'async-mutex';
|
||||
import {exec as unsafeExec, Output} from 'promisify-child-process';
|
||||
import {reportPlatformFailures} from 'flipper-common';
|
||||
import {exec as unsafeExec, Output, execFile} from 'promisify-child-process';
|
||||
import {DeviceTarget, DeviceType, reportPlatformFailures} from 'flipper-common';
|
||||
import {promises, constants} from 'fs';
|
||||
import memoize from 'lodash.memoize';
|
||||
import {notNull} from '../../utils/typeUtils';
|
||||
import {promisify} from 'util';
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
@@ -25,6 +24,25 @@ export type IdbConfig = {
|
||||
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
|
||||
const IDB_LOG_LEVEL = 'DEBUG';
|
||||
const LOG_TAG = 'iOSContainerUtility';
|
||||
@@ -32,30 +50,11 @@ const CMD_RECORD_THROTTLE_COUNT = 10;
|
||||
|
||||
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 idbCompanionDeviceListing = 0;
|
||||
let xcodeDeviceListing = 0;
|
||||
|
||||
async function isAvailable(idbPath: string): Promise<boolean> {
|
||||
export async function isIdbAvailable(idbPath: string): Promise<boolean> {
|
||||
if (!idbPath) {
|
||||
return false;
|
||||
}
|
||||
@@ -74,16 +73,50 @@ async function safeExec(
|
||||
return await unsafeExec(command).finally(release);
|
||||
}
|
||||
|
||||
async function queryTargetsWithXcode(
|
||||
context: any,
|
||||
export function getDeviceSetPath() {
|
||||
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>> {
|
||||
const cmd = 'xcrun xctrace list devices';
|
||||
const cmd = 'xcrun simctl list devices --json';
|
||||
const description = 'Query available devices with Xcode';
|
||||
const troubleshoot = `Xcode command line tools are not installed.
|
||||
Run 'xcode-select --install' from terminal.`;
|
||||
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
const {stdout} = await execFile('xcrun', [
|
||||
'simctl',
|
||||
...getDeviceSetPath(),
|
||||
'list',
|
||||
'devices',
|
||||
'--json',
|
||||
]);
|
||||
|
||||
if (!stdout) {
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
@@ -105,17 +138,22 @@ async function queryTargetsWithXcode(
|
||||
});
|
||||
}
|
||||
|
||||
return stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line))
|
||||
.filter(notNull)
|
||||
.filter(([_match, _name, _udid, isSim]) => !isSim)
|
||||
.map<DeviceTarget>(([_match, name, udid]) => {
|
||||
return {udid, type: 'physical', name};
|
||||
});
|
||||
const devices = JSON.parse(stdout.toString()).devices as {
|
||||
[key: string]: Array<XcodeTarget>;
|
||||
};
|
||||
|
||||
return Object.keys(devices).flatMap((key: string) =>
|
||||
devices[key]
|
||||
.filter((simulator: XcodeTarget) => isSimulatorAvailable(simulator))
|
||||
.map((simulator: XcodeTarget) => {
|
||||
return {
|
||||
...simulator,
|
||||
type: 'emulator',
|
||||
state: simulator.state.toLowerCase(),
|
||||
osVersion: getOSVersionFromXCRunOutput(key),
|
||||
} as DeviceTarget;
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
@@ -176,7 +214,7 @@ async function queryTargetsWithIdb(
|
||||
}
|
||||
}
|
||||
|
||||
async function queryTargetsWithIdbCompanion(
|
||||
async function _queryTargetsWithIdbCompanion(
|
||||
idbCompanionPath: string,
|
||||
isPhysicalDeviceEnabled: boolean,
|
||||
context: any,
|
||||
@@ -187,7 +225,7 @@ async function queryTargetsWithIdbCompanion(
|
||||
const troubleshoot = `Unable to locate idb_companion in '${idbCompanionPath}'.
|
||||
Try running sudo yum install -y fb-idb`;
|
||||
|
||||
if (await isAvailable(idbCompanionPath)) {
|
||||
if (await isIdbAvailable(idbCompanionPath)) {
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
if (!stdout) {
|
||||
@@ -244,9 +282,6 @@ async function queryTargetsWithIdbCompanion(
|
||||
|
||||
function parseIdbTarget(line: string): DeviceTarget | undefined {
|
||||
const parsed: IdbTarget = JSON.parse(line);
|
||||
if (parsed.state.toLocaleLowerCase() !== 'booted') {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
udid: parsed.udid,
|
||||
type:
|
||||
@@ -255,6 +290,7 @@ function parseIdbTarget(line: string): DeviceTarget | undefined {
|
||||
: ('physical' as DeviceType),
|
||||
name: parsed.name,
|
||||
osVersion: parsed.os_version,
|
||||
state: parsed.state?.toLocaleLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,45 +367,36 @@ async function idbDescribeTarget(
|
||||
async function targets(
|
||||
idbPath: string,
|
||||
isPhysicalDeviceEnabled: boolean,
|
||||
bootedOnly: boolean = false,
|
||||
context?: any,
|
||||
): Promise<Array<DeviceTarget>> {
|
||||
if (process.platform !== 'darwin') {
|
||||
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
|
||||
// is provided to Flipper via IDB_COMPANION environment variable,
|
||||
// use that instead and do not query other devices.
|
||||
// See stack of D36315576 for details
|
||||
if (process.env.IDB_COMPANION) {
|
||||
const target = await idbDescribeTarget(idbPath, context);
|
||||
return target ? [target] : [];
|
||||
return bootedFilter(target ? [target] : []);
|
||||
}
|
||||
|
||||
const isXcodeInstalled = await isXcodeDetected();
|
||||
if (!isXcodeInstalled) {
|
||||
if (!isPhysicalDeviceEnabled) {
|
||||
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);
|
||||
if (await memoize(isIdbAvailable)(idbPath)) {
|
||||
const targets = await queryTargetsWithIdb(idbPath, context);
|
||||
return bootedFilter(targets);
|
||||
} 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> {
|
||||
const isInstalled = await isAvailable(idbPath);
|
||||
const isInstalled = await isIdbAvailable(idbPath);
|
||||
if (!isInstalled) {
|
||||
throw new Error(
|
||||
`idb is required to use iOS devices. Install it with instructions
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {ChildProcess} from 'child_process';
|
||||
import type {IOSDeviceParams} from 'flipper-common';
|
||||
import type {DeviceTarget} from 'flipper-common';
|
||||
import path from 'path';
|
||||
import childProcess from 'child_process';
|
||||
import {exec} from 'promisify-child-process';
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ERR_NO_IDB_OR_XCODE_AVAILABLE,
|
||||
IOSBridge,
|
||||
makeIOSBridge,
|
||||
SimctlBridge,
|
||||
} from './IOSBridge';
|
||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||
import {getFlipperServerConfig} from '../../FlipperServerConfig';
|
||||
@@ -34,7 +33,7 @@ export class IOSDeviceManager {
|
||||
'MacOS',
|
||||
'PortForwardingMacApp',
|
||||
);
|
||||
simctlBridge: SimctlBridge = new SimctlBridge();
|
||||
ctlBridge: IOSBridge | undefined;
|
||||
|
||||
readonly certificateProvider: iOSCertificateProvider;
|
||||
|
||||
@@ -102,7 +101,7 @@ export class IOSDeviceManager {
|
||||
return this.processDevices(bridge, devices);
|
||||
}
|
||||
|
||||
private processDevices(bridge: IOSBridge, activeDevices: IOSDeviceParams[]) {
|
||||
private processDevices(bridge: IOSBridge, activeDevices: DeviceTarget[]) {
|
||||
const currentDeviceIDs = new Set(
|
||||
this.flipperServer
|
||||
.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() {
|
||||
try {
|
||||
const isDetected = await iosUtil.isXcodeDetected();
|
||||
if (this.idbConfig.enablePhysicalIOS) {
|
||||
this.startDevicePortForwarders();
|
||||
}
|
||||
@@ -144,11 +157,7 @@ export class IOSDeviceManager {
|
||||
// Check for version mismatch now for immediate error handling.
|
||||
await this.checkXcodeVersionMismatch();
|
||||
// Awaiting the promise here to trigger immediate error handling.
|
||||
const bridge = await makeIOSBridge(
|
||||
this.idbConfig.idbPath,
|
||||
isDetected,
|
||||
this.idbConfig.enablePhysicalIOS,
|
||||
);
|
||||
const bridge = await this.getBridge();
|
||||
await this.queryDevicesForever(bridge);
|
||||
} catch (err) {
|
||||
// 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 {
|
||||
return await this.simctlBridge.getActiveDevices(bootedOnly);
|
||||
const bridge = await this.getBridge();
|
||||
return await bridge.getActiveDevices(bootedOnly);
|
||||
} catch (e) {
|
||||
console.warn('Failed to query simulators:', e);
|
||||
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) {
|
||||
try {
|
||||
await this.queryDevices(bridge);
|
||||
|
||||
Reference in New Issue
Block a user