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.');
|
console.warn('Attaching iOS log listener continuously failed.');
|
||||||
return;
|
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) => {
|
this.log.on('error', (err: Error) => {
|
||||||
console.error('iOS log tailer error', err);
|
console.error('iOS log tailer error', err);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,18 @@
|
|||||||
* @format
|
* @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 =
|
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';
|
'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],
|
match[0],
|
||||||
).toEqual('/Applications/Xcode_12.4.0_fb.app/Contents/Developer');
|
).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(
|
async function queryDevices(
|
||||||
store: Store,
|
store: Store,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
iosBridge: IOSBridge,
|
iosBridge: IOSBridge,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return Promise.all([
|
const isXcodeInstalled = await iosUtil.isXcodeDetected();
|
||||||
checkXcodeVersionMismatch(store),
|
return Promise.all(
|
||||||
getSimulators(store, true).then((devices) => {
|
getAllPromisesForQueryingDevices(
|
||||||
processDevices(store, logger, iosBridge, devices, 'emulator');
|
store,
|
||||||
}),
|
logger,
|
||||||
getActiveDevices(store.getState().settingsState.idbPath).then(
|
iosBridge,
|
||||||
(devices: IOSDeviceParams[]) => {
|
isXcodeInstalled,
|
||||||
processDevices(store, logger, iosBridge, devices, 'physical');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processDevices(
|
function processDevices(
|
||||||
@@ -224,8 +249,11 @@ export async function launchSimulator(udid: string): Promise<any> {
|
|||||||
await promisify(execFile)('open', ['-a', 'simulator']);
|
await promisify(execFile)('open', ['-a', 'simulator']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveDevices(idbPath: string): Promise<Array<IOSDeviceParams>> {
|
function getActiveDevices(
|
||||||
return iosUtil.targets(idbPath).catch((e) => {
|
idbPath: string,
|
||||||
|
isPhysicalDeviceEnabled: boolean,
|
||||||
|
): Promise<Array<IOSDeviceParams>> {
|
||||||
|
return iosUtil.targets(idbPath, isPhysicalDeviceEnabled).catch((e) => {
|
||||||
console.error('Failed to get active iOS devices:', e.message);
|
console.error('Failed to get active iOS devices:', e.message);
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
@@ -279,25 +307,19 @@ async function checkXcodeVersionMismatch(store: Store) {
|
|||||||
console.error('Failed to determine Xcode version:', e);
|
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) => {
|
export default (store: Store, logger: Logger) => {
|
||||||
if (!store.getState().settingsState.enableIOS) {
|
if (!store.getState().settingsState.enableIOS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isXcodeDetected().then((isDetected) => {
|
iosUtil.isXcodeDetected().then((isDetected) => {
|
||||||
store.dispatch(setXcodeDetected(isDetected));
|
store.dispatch(setXcodeDetected(isDetected));
|
||||||
if (isDetected) {
|
if (store.getState().settingsState.enablePhysicalIOS) {
|
||||||
if (store.getState().settingsState.enablePhysicalIOS) {
|
startDevicePortForwarders();
|
||||||
startDevicePortForwarders();
|
|
||||||
}
|
|
||||||
return makeIOSBridge(
|
|
||||||
store.getState().settingsState.idbPath,
|
|
||||||
).then((iosBridge) => queryDevicesForever(store, logger, iosBridge));
|
|
||||||
}
|
}
|
||||||
|
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 Promise.resolve(matches[1]);
|
||||||
}
|
}
|
||||||
return iosUtil
|
return iosUtil
|
||||||
.targets(this.store.getState().settingsState.idbPath)
|
.targets(
|
||||||
|
this.store.getState().settingsState.idbPath,
|
||||||
|
this.store.getState().settingsState.enablePhysicalIOS,
|
||||||
|
)
|
||||||
.then((targets) => {
|
.then((targets) => {
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
throw new Error('No iOS devices found');
|
throw new Error('No iOS devices found');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import fs from 'fs';
|
|||||||
import child_process from 'child_process';
|
import child_process from 'child_process';
|
||||||
|
|
||||||
export interface IOSBridge {
|
export interface IOSBridge {
|
||||||
startLogListener: (
|
startLogListener?: (
|
||||||
udid: string,
|
udid: string,
|
||||||
) => child_process.ChildProcessWithoutNullStreams;
|
) => child_process.ChildProcessWithoutNullStreams;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ const LOG_EXTRA_ARGS = [
|
|||||||
'--info',
|
'--info',
|
||||||
];
|
];
|
||||||
|
|
||||||
function idbStartLogListener(
|
export function idbStartLogListener(
|
||||||
idbPath: string,
|
idbPath: string,
|
||||||
udid: string,
|
udid: string,
|
||||||
): child_process.ChildProcessWithoutNullStreams {
|
): 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
|
const deviceSetPath = process.env.DEVICE_SET_PATH
|
||||||
? ['--set', process.env.DEVICE_SET_PATH]
|
? ['--set', process.env.DEVICE_SET_PATH]
|
||||||
: [];
|
: [];
|
||||||
@@ -67,8 +67,14 @@ function xcrunStartLogListener(udid: string) {
|
|||||||
|
|
||||||
export async function makeIOSBridge(
|
export async function makeIOSBridge(
|
||||||
idbPath: string,
|
idbPath: string,
|
||||||
|
isXcodeDetected: boolean,
|
||||||
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
|
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
|
||||||
): Promise<IOSBridge> {
|
): 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)) {
|
if (await isAvailableFn(idbPath)) {
|
||||||
return {
|
return {
|
||||||
startLogListener: idbStartLogListener.bind(null, idbPath),
|
startLogListener: idbStartLogListener.bind(null, idbPath),
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {mocked} from 'ts-jest/utils';
|
|||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
const spawn = mocked(childProcess.spawn);
|
const spawn = mocked(childProcess.spawn);
|
||||||
|
|
||||||
test('uses xcrun with no idb', async () => {
|
test('uses xcrun with no idb when xcode is detected', async () => {
|
||||||
const ib = await makeIOSBridge('');
|
const ib = await makeIOSBridge('', true);
|
||||||
ib.startLogListener('deadbeef');
|
expect(ib.startLogListener).toBeDefined();
|
||||||
|
|
||||||
|
ib.startLogListener!('deadbeef');
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith(
|
expect(spawn).toHaveBeenCalledWith(
|
||||||
'xcrun',
|
'xcrun',
|
||||||
@@ -37,9 +39,11 @@ test('uses xcrun with no idb', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses idb when present', async () => {
|
test('uses idb when present and xcode detected', async () => {
|
||||||
const ib = await makeIOSBridge('/usr/local/bin/idb', async (_) => true);
|
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
|
||||||
ib.startLogListener('deadbeef');
|
expect(ib.startLogListener).toBeDefined();
|
||||||
|
|
||||||
|
ib.startLogListener!('deadbeef');
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith(
|
expect(spawn).toHaveBeenCalledWith(
|
||||||
'/usr/local/bin/idb',
|
'/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 {promises, constants} from 'fs';
|
||||||
import memoize from 'lodash.memoize';
|
import memoize from 'lodash.memoize';
|
||||||
import {notNull} from './typeUtils';
|
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
|
// Use debug to get helpful logs when idb fails
|
||||||
const idbLogLevel = 'DEBUG';
|
const idbLogLevel = 'DEBUG';
|
||||||
@@ -54,10 +59,79 @@ function safeExec(
|
|||||||
.then((release) => unsafeExec(command).finally(release));
|
.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') {
|
if (process.platform !== 'darwin') {
|
||||||
return [];
|
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
|
// Not all users have idb installed because you can still use
|
||||||
// Flipper with Simulators without it.
|
// 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 {
|
export default {
|
||||||
isAvailable,
|
isAvailable,
|
||||||
targets,
|
targets,
|
||||||
push,
|
push,
|
||||||
pull,
|
pull,
|
||||||
|
isXcodeDetected,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user