Detect Physical iOS device without Xcode
Summary: This diff adds the support of detecting physical device in Flipper even if the xcode is not installed and there is no cli tool installed. See the demo. Reviewed By: timur-valiev Differential Revision: D26816588 fbshipit-source-id: 5f052998fcbe5c51385222d16df0e1855177b552
This commit is contained in:
committed by
Facebook GitHub Bot
parent
11879c127b
commit
a2d559c8c0
@@ -92,8 +92,10 @@ export default class IOSDevice extends BaseDevice {
|
||||
console.warn('Attaching iOS log listener continuously failed.');
|
||||
return;
|
||||
}
|
||||
if (!this.log) {
|
||||
this.log = iOSBridge.startLogListener(this.serial);
|
||||
|
||||
const logListener = iOSBridge.startLogListener;
|
||||
if (!this.log && logListener) {
|
||||
this.log = logListener(this.serial);
|
||||
this.log.on('error', (err: Error) => {
|
||||
console.error('iOS log tailer error', err);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,18 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {parseXcodeFromCoreSimPath} from '../iOSDevice';
|
||||
import {
|
||||
parseXcodeFromCoreSimPath,
|
||||
getAllPromisesForQueryingDevices,
|
||||
} from '../iOSDevice';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import reducers, {State} from '../../reducers/index';
|
||||
import {getInstance} from '../../fb-stubs/Logger';
|
||||
|
||||
const mockStore = configureStore<State, {}>([])(
|
||||
reducers(undefined, {type: 'INIT'}),
|
||||
);
|
||||
const logger = getInstance();
|
||||
|
||||
const standardCoresimulatorLog =
|
||||
'username 1264 0.0 0.1 5989740 41648 ?? Ss 2:23PM 0:12.92 /Applications/Xcode_12.4.0_fb.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/libexec/mobileassetd';
|
||||
@@ -48,3 +59,23 @@ test('test parseXcodeFromCoreSimPath from standard locations', () => {
|
||||
match[0],
|
||||
).toEqual('/Applications/Xcode_12.4.0_fb.app/Contents/Developer');
|
||||
});
|
||||
|
||||
test('test getAllPromisesForQueryingDevices when xcode detected', () => {
|
||||
const promises = getAllPromisesForQueryingDevices(
|
||||
mockStore,
|
||||
logger,
|
||||
{},
|
||||
true,
|
||||
);
|
||||
expect(promises.length).toEqual(3);
|
||||
});
|
||||
|
||||
test('test getAllPromisesForQueryingDevices when xcode is not detected', () => {
|
||||
const promises = getAllPromisesForQueryingDevices(
|
||||
mockStore,
|
||||
logger,
|
||||
{},
|
||||
false,
|
||||
);
|
||||
expect(promises.length).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -93,22 +93,47 @@ if (typeof window !== 'undefined') {
|
||||
});
|
||||
}
|
||||
|
||||
export function getAllPromisesForQueryingDevices(
|
||||
store: Store,
|
||||
logger: Logger,
|
||||
iosBridge: IOSBridge,
|
||||
isXcodeDetected: boolean,
|
||||
): Array<Promise<any>> {
|
||||
const promArray = [
|
||||
getActiveDevices(
|
||||
store.getState().settingsState.idbPath,
|
||||
store.getState().settingsState.enablePhysicalIOS,
|
||||
).then((devices: IOSDeviceParams[]) => {
|
||||
processDevices(store, logger, iosBridge, devices, 'physical');
|
||||
}),
|
||||
];
|
||||
if (isXcodeDetected) {
|
||||
promArray.push(
|
||||
...[
|
||||
checkXcodeVersionMismatch(store),
|
||||
getSimulators(store, true).then((devices) => {
|
||||
processDevices(store, logger, iosBridge, devices, 'emulator');
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
return promArray;
|
||||
}
|
||||
|
||||
async function queryDevices(
|
||||
store: Store,
|
||||
logger: Logger,
|
||||
iosBridge: IOSBridge,
|
||||
): Promise<any> {
|
||||
return Promise.all([
|
||||
checkXcodeVersionMismatch(store),
|
||||
getSimulators(store, true).then((devices) => {
|
||||
processDevices(store, logger, iosBridge, devices, 'emulator');
|
||||
}),
|
||||
getActiveDevices(store.getState().settingsState.idbPath).then(
|
||||
(devices: IOSDeviceParams[]) => {
|
||||
processDevices(store, logger, iosBridge, devices, 'physical');
|
||||
},
|
||||
const isXcodeInstalled = await iosUtil.isXcodeDetected();
|
||||
return Promise.all(
|
||||
getAllPromisesForQueryingDevices(
|
||||
store,
|
||||
logger,
|
||||
iosBridge,
|
||||
isXcodeInstalled,
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
function processDevices(
|
||||
@@ -224,8 +249,11 @@ export async function launchSimulator(udid: string): Promise<any> {
|
||||
await promisify(execFile)('open', ['-a', 'simulator']);
|
||||
}
|
||||
|
||||
function getActiveDevices(idbPath: string): Promise<Array<IOSDeviceParams>> {
|
||||
return iosUtil.targets(idbPath).catch((e) => {
|
||||
function getActiveDevices(
|
||||
idbPath: string,
|
||||
isPhysicalDeviceEnabled: boolean,
|
||||
): Promise<Array<IOSDeviceParams>> {
|
||||
return iosUtil.targets(idbPath, isPhysicalDeviceEnabled).catch((e) => {
|
||||
console.error('Failed to get active iOS devices:', e.message);
|
||||
return [];
|
||||
});
|
||||
@@ -279,25 +307,19 @@ async function checkXcodeVersionMismatch(store: Store) {
|
||||
console.error('Failed to determine Xcode version:', e);
|
||||
}
|
||||
}
|
||||
async function isXcodeDetected(): Promise<boolean> {
|
||||
return exec('xcode-select -p')
|
||||
.then((_) => true)
|
||||
.catch((_) => false);
|
||||
}
|
||||
|
||||
export default (store: Store, logger: Logger) => {
|
||||
if (!store.getState().settingsState.enableIOS) {
|
||||
return;
|
||||
}
|
||||
isXcodeDetected().then((isDetected) => {
|
||||
iosUtil.isXcodeDetected().then((isDetected) => {
|
||||
store.dispatch(setXcodeDetected(isDetected));
|
||||
if (isDetected) {
|
||||
if (store.getState().settingsState.enablePhysicalIOS) {
|
||||
startDevicePortForwarders();
|
||||
}
|
||||
return makeIOSBridge(
|
||||
store.getState().settingsState.idbPath,
|
||||
).then((iosBridge) => queryDevicesForever(store, logger, iosBridge));
|
||||
if (store.getState().settingsState.enablePhysicalIOS) {
|
||||
startDevicePortForwarders();
|
||||
}
|
||||
return makeIOSBridge(
|
||||
store.getState().settingsState.idbPath,
|
||||
isDetected,
|
||||
).then((iosBridge) => queryDevicesForever(store, logger, iosBridge));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -425,7 +425,10 @@ export default class CertificateProvider {
|
||||
return Promise.resolve(matches[1]);
|
||||
}
|
||||
return iosUtil
|
||||
.targets(this.store.getState().settingsState.idbPath)
|
||||
.targets(
|
||||
this.store.getState().settingsState.idbPath,
|
||||
this.store.getState().settingsState.enablePhysicalIOS,
|
||||
)
|
||||
.then((targets) => {
|
||||
if (targets.length === 0) {
|
||||
throw new Error('No iOS devices found');
|
||||
|
||||
@@ -11,7 +11,7 @@ import fs from 'fs';
|
||||
import child_process from 'child_process';
|
||||
|
||||
export interface IOSBridge {
|
||||
startLogListener: (
|
||||
startLogListener?: (
|
||||
udid: string,
|
||||
) => child_process.ChildProcessWithoutNullStreams;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const LOG_EXTRA_ARGS = [
|
||||
'--info',
|
||||
];
|
||||
|
||||
function idbStartLogListener(
|
||||
export function idbStartLogListener(
|
||||
idbPath: string,
|
||||
udid: string,
|
||||
): child_process.ChildProcessWithoutNullStreams {
|
||||
@@ -46,7 +46,7 @@ function idbStartLogListener(
|
||||
);
|
||||
}
|
||||
|
||||
function xcrunStartLogListener(udid: string) {
|
||||
export function xcrunStartLogListener(udid: string) {
|
||||
const deviceSetPath = process.env.DEVICE_SET_PATH
|
||||
? ['--set', process.env.DEVICE_SET_PATH]
|
||||
: [];
|
||||
@@ -67,8 +67,14 @@ function xcrunStartLogListener(udid: string) {
|
||||
|
||||
export async function makeIOSBridge(
|
||||
idbPath: string,
|
||||
isXcodeDetected: boolean,
|
||||
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
|
||||
): Promise<IOSBridge> {
|
||||
if (!isXcodeDetected) {
|
||||
// iOS Physical Device can still get detected without Xcode. In this case there is no way to setup log listener yet.
|
||||
// This will not be the case, idb team is working on making idb log work without XCode atleast for physical device.
|
||||
return {};
|
||||
}
|
||||
if (await isAvailableFn(idbPath)) {
|
||||
return {
|
||||
startLogListener: idbStartLogListener.bind(null, idbPath),
|
||||
|
||||
@@ -14,9 +14,11 @@ import {mocked} from 'ts-jest/utils';
|
||||
jest.mock('child_process');
|
||||
const spawn = mocked(childProcess.spawn);
|
||||
|
||||
test('uses xcrun with no idb', async () => {
|
||||
const ib = await makeIOSBridge('');
|
||||
ib.startLogListener('deadbeef');
|
||||
test('uses xcrun with no idb when xcode is detected', async () => {
|
||||
const ib = await makeIOSBridge('', true);
|
||||
expect(ib.startLogListener).toBeDefined();
|
||||
|
||||
ib.startLogListener!('deadbeef');
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'xcrun',
|
||||
@@ -37,9 +39,11 @@ test('uses xcrun with no idb', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('uses idb when present', async () => {
|
||||
const ib = await makeIOSBridge('/usr/local/bin/idb', async (_) => true);
|
||||
ib.startLogListener('deadbeef');
|
||||
test('uses idb when present and xcode detected', async () => {
|
||||
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
|
||||
expect(ib.startLogListener).toBeDefined();
|
||||
|
||||
ib.startLogListener!('deadbeef');
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'/usr/local/bin/idb',
|
||||
@@ -58,3 +62,8 @@ test('uses idb when present', async () => {
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
test('uses no log listener when xcode is not detected', async () => {
|
||||
const ib = await makeIOSBridge('', false);
|
||||
expect(ib.startLogListener).toBeUndefined();
|
||||
});
|
||||
|
||||
43
desktop/app/src/utils/__tests__/iOSContainerUtility.node.tsx
Normal file
43
desktop/app/src/utils/__tests__/iOSContainerUtility.node.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {queryTargetsWithoutXcodeDependency} from '../iOSContainerUtility';
|
||||
|
||||
test('uses idbcompanion command for queryTargetsWithoutXcodeDependency', async () => {
|
||||
const mockedExec = jest.fn((_) =>
|
||||
Promise.resolve({
|
||||
stdout: '{"udid": "udid", "type": "physical", "name": "name"}',
|
||||
stderr: '{ "msg": "mocked stderr"}',
|
||||
}),
|
||||
);
|
||||
await queryTargetsWithoutXcodeDependency(
|
||||
'idbCompanionPath',
|
||||
true,
|
||||
(_) => Promise.resolve(true),
|
||||
mockedExec,
|
||||
);
|
||||
|
||||
expect(mockedExec).toBeCalledWith('idbCompanionPath --list 1 --only device');
|
||||
});
|
||||
|
||||
test('do not call idbcompanion if the path does not exist', async () => {
|
||||
const mockedExec = jest.fn((_) =>
|
||||
Promise.resolve({
|
||||
stdout: '{"udid": "udid", "type": "physical", "name": "name"}',
|
||||
stderr: '{"msg": "mocked stderr"}',
|
||||
}),
|
||||
);
|
||||
await queryTargetsWithoutXcodeDependency(
|
||||
'idbCompanionPath',
|
||||
true,
|
||||
(_) => Promise.resolve(false),
|
||||
mockedExec,
|
||||
);
|
||||
expect(mockedExec).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -14,6 +14,11 @@ import {reportPlatformFailures} from './metrics';
|
||||
import {promises, constants} from 'fs';
|
||||
import memoize from 'lodash.memoize';
|
||||
import {notNull} from './typeUtils';
|
||||
import {promisify} from 'util';
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
// Use debug to get helpful logs when idb fails
|
||||
const idbLogLevel = 'DEBUG';
|
||||
@@ -54,10 +59,79 @@ function safeExec(
|
||||
.then((release) => unsafeExec(command).finally(release));
|
||||
}
|
||||
|
||||
async function targets(idbPath: string): Promise<Array<DeviceTarget>> {
|
||||
export async function queryTargetsWithoutXcodeDependency(
|
||||
idbCompanionPath: string,
|
||||
isPhysicalDeviceEnabled: boolean,
|
||||
isAvailableFunc: (idbPath: string) => Promise<boolean>,
|
||||
safeExecFunc: (
|
||||
command: string,
|
||||
) => Promise<{stdout: string; stderr: string} | Output>,
|
||||
): Promise<Array<DeviceTarget>> {
|
||||
if (await isAvailableFunc(idbCompanionPath)) {
|
||||
return safeExecFunc(`${idbCompanionPath} --list 1 --only device`)
|
||||
.then(({stdout}) =>
|
||||
// It is safe to assume this to be non-null as it only turns null
|
||||
// if the output redirection is misconfigured:
|
||||
// https://stackoverflow.com/questions/27786228/node-child-process-spawn-stdout-returning-as-null
|
||||
stdout!
|
||||
.toString()
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
.filter(({type}: IdbTarget) => type !== 'simulator')
|
||||
.map<DeviceTarget>((target: IdbTarget) => {
|
||||
return {udid: target.udid, type: 'physical', name: target.name};
|
||||
}),
|
||||
)
|
||||
.then((devices) => {
|
||||
if (devices.length > 0 && !isPhysicalDeviceEnabled) {
|
||||
// TODO: Show a notification to enable the toggle or integrate Doctor to better suggest this advice.
|
||||
console.warn(
|
||||
'You are trying to connect Physical Device. Please enable the toggle "Enable physical iOS device" from the setting screen.',
|
||||
);
|
||||
}
|
||||
return devices;
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.warn(
|
||||
'Failed to query idb_companion --list 1 --only device for physical targets:',
|
||||
e,
|
||||
);
|
||||
return [];
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
`Unable to locate idb_companion in ${idbCompanionPath}. Try running sudo yum install -y fb-idb`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function targets(
|
||||
idbPath: string,
|
||||
isPhysicalDeviceEnabled: boolean,
|
||||
): Promise<Array<DeviceTarget>> {
|
||||
if (process.platform !== 'darwin') {
|
||||
return [];
|
||||
}
|
||||
const isXcodeInstalled = await isXcodeDetected();
|
||||
if (!isXcodeInstalled) {
|
||||
if (!isPhysicalDeviceEnabled) {
|
||||
// TODO: Show a notification to enable the toggle or integrate Doctor to better suggest this advice.
|
||||
console.warn(
|
||||
'You are trying to connect Physical Device. Please enable the toggle "Enable physical iOS device" from the setting screen.',
|
||||
);
|
||||
}
|
||||
const idbCompanionPath = path.dirname(idbPath) + '/idb_companion';
|
||||
return queryTargetsWithoutXcodeDependency(
|
||||
idbCompanionPath,
|
||||
isPhysicalDeviceEnabled,
|
||||
isAvailable,
|
||||
safeExec,
|
||||
);
|
||||
}
|
||||
|
||||
// Not all users have idb installed because you can still use
|
||||
// Flipper with Simulators without it.
|
||||
@@ -185,9 +259,18 @@ function wrapWithErrorMessage<T>(p: Promise<T>): Promise<T> {
|
||||
});
|
||||
}
|
||||
|
||||
async function isXcodeDetected(): Promise<boolean> {
|
||||
return exec('xcode-select -p')
|
||||
.then(({stdout}) => {
|
||||
return fs.pathExists(stdout);
|
||||
})
|
||||
.catch((_) => false);
|
||||
}
|
||||
|
||||
export default {
|
||||
isAvailable,
|
||||
targets,
|
||||
push,
|
||||
pull,
|
||||
isXcodeDetected,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user