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
@@ -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';
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,32 +284,11 @@ 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;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchSimulator(udid: string): Promise<any> {
|
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) {
|
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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user