Files
flipper/desktop/app/src/dispatcher/iOSDevice.tsx
Michel Weststrate b94e426261 Make iOS device detection faster
Summary:
I noticed that Android device detection is _much_ faster than iOS, so I tried to optimize it a bit

1. Removed a test run on `instruments -s`. That command is really slow (easily 5 secs), and the check has become redundant since Doctor already does a similar check
2. When querying for devices, it tries to fetch emulated and physical devices in parallel, but only processes the results after both have finished. However, finding simulators is almost instant, while querying the physical devices takes ~5 seconds ore more. So in this diff we process the found devices till in parallel, rather than waiting until both have arrived

This diff reduces the time until the ios simulator + FB app is detected from 28 to 4 seconds on my machine.

Changelog: Improved the startup sequence for emulated iOS devices, so that devices and apps connect a lot faster after starting Flipper

Reviewed By: jknoxville

Differential Revision: D21907597

fbshipit-source-id: 73edf0b04c4ad8367e04557e33f4c0d9e9bcd710
2020-06-08 03:44:51 -07:00

266 lines
7.9 KiB
TypeScript

/**
* 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 {ChildProcess} from 'child_process';
import {Store} from '../reducers/index';
import {setXcodeDetected} from '../reducers/application';
import {Logger} from '../fb-interfaces/Logger';
import {DeviceType} from '../devices/BaseDevice';
import {promisify} from 'util';
import path from 'path';
import child_process from 'child_process';
const execFile = child_process.execFile;
import iosUtil from '../utils/iOSContainerUtility';
import IOSDevice from '../devices/IOSDevice';
import isProduction from '../utils/isProduction';
import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
type iOSSimulatorDevice = {
state: 'Booted' | 'Shutdown' | 'Shutting Down';
availability?: string;
isAvailable?: 'YES' | 'NO' | true | false;
name: string;
udid: string;
};
type IOSDeviceParams = {udid: string; type: DeviceType; name: string};
const exec = promisify(child_process.exec);
let portForwarders: Array<ChildProcess> = [];
function isAvailable(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
);
}
const portforwardingClient = isProduction()
? path.resolve(
__dirname,
'PortForwardingMacApp.app/Contents/MacOS/PortForwardingMacApp',
)
: 'PortForwardingMacApp.app/Contents/MacOS/PortForwardingMacApp';
function forwardPort(port: number, multiplexChannelPort: number) {
return execFile(portforwardingClient, [
`-portForward=${port}`,
`-multiplexChannelPort=${multiplexChannelPort}`,
]);
}
function startDevicePortForwarders(): void {
if (portForwarders.length > 0) {
// Only ever start them once.
return;
}
// start port forwarding server for real device connections
portForwarders = [forwardPort(8089, 8079), forwardPort(8088, 8078)];
}
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
portForwarders.forEach((process) => process.kill());
});
}
async function queryDevices(store: Store, logger: Logger): Promise<any> {
return Promise.all([
checkXcodeVersionMismatch(store),
getActiveSimulators().then((devices) => {
processDevices(store, logger, devices, 'emulator');
}),
getActiveDevices().then((devices) => {
processDevices(store, logger, devices, 'physical');
}),
]);
}
function processDevices(
store: Store,
logger: Logger,
activeDevices: IOSDeviceParams[],
type: 'physical' | 'emulator',
) {
const {connections} = store.getState();
const currentDeviceIDs: Set<string> = new Set(
connections.devices
.filter(
(device) =>
device instanceof IOSDevice &&
device.deviceType === type &&
!device.isArchived,
)
.map((device) => device.serial),
);
for (const {udid, type, name} of activeDevices) {
if (currentDeviceIDs.has(udid)) {
currentDeviceIDs.delete(udid);
} else {
logger.track('usage', 'register-device', {
os: 'iOS',
type: type,
name: name,
serial: udid,
});
const iOSDevice = new IOSDevice(udid, type, name);
iOSDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({
type: 'REGISTER_DEVICE',
payload: iOSDevice,
});
registerDeviceCallbackOnPlugins(
store,
store.getState().plugins.devicePlugins,
store.getState().plugins.clientPlugins,
iOSDevice,
);
}
}
if (currentDeviceIDs.size > 0) {
currentDeviceIDs.forEach((id) =>
logger.track('usage', 'unregister-device', {os: 'iOS', serial: id}),
);
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: currentDeviceIDs,
});
}
}
function getActiveSimulators(): Promise<Array<IOSDeviceParams>> {
const deviceSetPath = process.env.DEVICE_SET_PATH
? ['--set', process.env.DEVICE_SET_PATH]
: [];
return promisify(execFile)(
'xcrun',
['simctl', ...deviceSetPath, 'list', 'devices', '--json'],
{
encoding: 'utf8',
},
)
.then(({stdout}) => JSON.parse(stdout).devices)
.then((simulatorDevices: Array<iOSSimulatorDevice>) => {
const simulators: Array<iOSSimulatorDevice> = Object.values(
simulatorDevices,
).reduce((acc: Array<iOSSimulatorDevice>, cv) => acc.concat(cv), []);
return simulators
.filter(
(simulator) => simulator.state === 'Booted' && isAvailable(simulator),
)
.map((simulator) => {
return {
udid: simulator.udid,
type: 'emulator',
name: simulator.name,
} as IOSDeviceParams;
});
})
.catch((_) => []);
}
function getActiveDevices(): Promise<Array<IOSDeviceParams>> {
return iosUtil.targets().catch((e) => {
console.error(e.message);
return [];
});
}
function queryDevicesForever(store: Store, logger: Logger) {
return queryDevices(store, logger)
.then(() => {
// It's important to schedule the next check AFTER the current one has completed
// to avoid simultaneous queries which can cause multiple user input prompts.
setTimeout(() => queryDevicesForever(store, logger), 3000);
})
.catch((err) => {
console.error(err);
});
}
let xcodeVersionMismatchFound = false;
async function checkXcodeVersionMismatch(store: Store) {
if (xcodeVersionMismatchFound) {
return;
}
try {
let {stdout: xcodeCLIVersion} = await exec('xcode-select -p');
xcodeCLIVersion = xcodeCLIVersion.trim();
const {stdout} = await exec('ps aux | grep CoreSimulator');
for (const line of stdout.split('\n')) {
const match = line.match(
/\/Applications\/Xcode[^/]*\.app\/Contents\/Developer/,
);
const runningVersion = match && match.length > 0 ? match[0].trim() : null;
if (runningVersion && runningVersion !== xcodeCLIVersion) {
const errorMessage = `Xcode version mismatch: Simulator is running from "${runningVersion}" while Xcode CLI is "${xcodeCLIVersion}". Running "xcode-select --switch ${runningVersion}" can fix this.`;
store.dispatch({
type: 'SERVER_ERROR',
payload: {
message: errorMessage,
details:
"You might want to run 'sudo xcode-select -s /Applications/Xcode.app/Contents/Developer'",
urgent: true,
},
});
// Fire a console.error as well, so that it gets reported to the backend.
console.error(errorMessage);
xcodeVersionMismatchFound = true;
break;
}
}
} catch (e) {
console.error(e);
}
}
async function isXcodeDetected(): Promise<boolean> {
return exec('xcode-select -p')
.then((_) => true)
.catch((_) => false);
}
export async function getActiveDevicesAndSimulators(): Promise<
Array<IOSDevice>
> {
const activeDevices: Array<Array<IOSDeviceParams>> = await Promise.all([
getActiveSimulators(),
getActiveDevices(),
]);
const allDevices = activeDevices[0].concat(activeDevices[1]);
return allDevices.map((device) => {
const {udid, type, name} = device;
return new IOSDevice(udid, type, name);
});
}
export default (store: Store, logger: Logger) => {
// monitoring iOS devices only available on MacOS.
if (process.platform !== 'darwin') {
return;
}
if (!store.getState().settingsState.enableIOS) {
return;
}
isXcodeDetected().then((isDetected) => {
store.dispatch(setXcodeDetected(isDetected));
if (isDetected) {
startDevicePortForwarders();
return queryDevicesForever(store, logger);
}
});
};