From ea58f2b05078adc066c3aaecd53f199447514aea Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 17 Aug 2021 07:50:43 -0700 Subject: [PATCH] Decouple iOS devices from Store / core Summary: Decouple iOS device detection from Redux Reviewed By: timur-valiev Differential Revision: D30309258 fbshipit-source-id: 74b4e3dd2e6b83fcefc75909794c39bfc8c987cf --- desktop/app/src/chrome/SettingsSheet.tsx | 5 +- desktop/app/src/dispatcher/flipperServer.tsx | 6 +- desktop/app/src/reducers/application.tsx | 15 - .../appinspect/LaunchEmulator.tsx | 34 +- desktop/app/src/server/FlipperServer.tsx | 16 +- .../devices/android/androidDeviceManager.tsx | 36 +- .../devices/ios/__tests__/iOSDevice.node.tsx | 24 +- .../server/devices/ios/iOSDeviceManager.tsx | 482 ++++++++---------- .../src/server/utils/CertificateProvider.tsx | 6 +- 9 files changed, 291 insertions(+), 333 deletions(-) diff --git a/desktop/app/src/chrome/SettingsSheet.tsx b/desktop/app/src/chrome/SettingsSheet.tsx index 91880fe27..e6601c85b 100644 --- a/desktop/app/src/chrome/SettingsSheet.tsx +++ b/desktop/app/src/chrome/SettingsSheet.tsx @@ -357,10 +357,11 @@ class SettingsSheet extends Component { } export default connect( - ({settingsState, launcherSettingsState, application}) => ({ + ({settingsState, launcherSettingsState, connections}) => ({ settings: settingsState, launcherSettings: launcherSettingsState, - isXcodeDetected: application.xcodeCommandLineToolsDetected, + isXcodeDetected: + connections.flipperServer?.ios.xcodeCommandLineToolsDetected ?? false, }), {updateSettings, updateLauncherSettings}, )(withTrackingScope(SettingsSheet)); diff --git a/desktop/app/src/dispatcher/flipperServer.tsx b/desktop/app/src/dispatcher/flipperServer.tsx index 0404c0fe2..a1297f846 100644 --- a/desktop/app/src/dispatcher/flipperServer.tsx +++ b/desktop/app/src/dispatcher/flipperServer.tsx @@ -16,11 +16,15 @@ import Client from '../Client'; import {notification} from 'antd'; export default async (store: Store, logger: Logger) => { - const {enableAndroid, androidHome} = store.getState().settingsState; + const {enableAndroid, androidHome, idbPath, enableIOS, enablePhysicalIOS} = + store.getState().settingsState; const server = new FlipperServer( { enableAndroid, androidHome, + idbPath, + enableIOS, + enablePhysicalIOS, serverPorts: store.getState().application.serverPorts, }, store, diff --git a/desktop/app/src/reducers/application.tsx b/desktop/app/src/reducers/application.tsx index e5fa1c83e..0e818ee65 100644 --- a/desktop/app/src/reducers/application.tsx +++ b/desktop/app/src/reducers/application.tsx @@ -86,7 +86,6 @@ export type State = { serverPorts: ServerPorts; launcherMsg: LauncherMsg; statusMessages: Array; - xcodeCommandLineToolsDetected: boolean; pastedToken?: string; }; @@ -149,12 +148,6 @@ export type Action = type: 'REMOVE_STATUS_MSG'; payload: {msg: string; sender: string}; } - | { - type: 'SET_XCODE_DETECTED'; - payload: { - isDetected: boolean; - }; - } | { type: 'SET_PASTED_TOKEN'; payload?: string; @@ -177,7 +170,6 @@ export const initialState: () => State = () => ({ message: '', }, statusMessages: [], - xcodeCommandLineToolsDetected: false, trackingTimeline: [], }); @@ -288,8 +280,6 @@ export default function reducer( return {...state, statusMessages}; } return state; - } else if (action.type === 'SET_XCODE_DETECTED') { - return {...state, xcodeCommandLineToolsDetected: action.payload.isDetected}; } else if (action.type === 'SET_PASTED_TOKEN') { return produce(state, (draft) => { draft.pastedToken = action.payload; @@ -368,11 +358,6 @@ export const removeStatusMessage = (payload: StatusMessageType): Action => ({ payload, }); -export const setXcodeDetected = (isDetected: boolean): Action => ({ - type: 'SET_XCODE_DETECTED', - payload: {isDetected}, -}); - export const setPastedToken = (pastedToken?: string): Action => ({ type: 'SET_PASTED_TOKEN', payload: pastedToken, diff --git a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx index cbb5604c3..82038f9f5 100644 --- a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx @@ -21,8 +21,7 @@ import {launchEmulator} from '../../server/devices/android/AndroidDevice'; import {Layout, renderReactRoot, withTrackingScope} from 'flipper-plugin'; import {Provider} from 'react-redux'; import { - launchSimulator, - getSimulators, + launchSimulator, // TODO: move to iOSDeviceManager IOSDeviceParams, } from '../../server/devices/ios/iOSDeviceManager'; import GK from '../../fb-stubs/GK'; @@ -39,19 +38,16 @@ export function showEmulatorLauncher(store: Store) { } function LaunchEmulatorContainer({onClose}: {onClose: () => void}) { - const store = useStore(); const flipperServer = useStore((state) => state.connections.flipperServer); return ( flipperServer!.ios.getSimulators(false)} getEmulators={() => flipperServer!.android.getAndroidEmulators()} /> ); } -type GetSimulators = typeof getSimulators; - export const LaunchEmulatorDialog = withTrackingScope( function LaunchEmulatorDialog({ onClose, @@ -59,7 +55,7 @@ export const LaunchEmulatorDialog = withTrackingScope( getEmulators, }: { onClose: () => void; - getSimulators: GetSimulators; + getSimulators: () => Promise; getEmulators: () => Promise; }) { const iosEnabled = useStore((state) => state.settingsState.enableIOS); @@ -74,15 +70,19 @@ export const LaunchEmulatorDialog = withTrackingScope( if (!iosEnabled) { return; } - getSimulators(store, false).then((emulators) => { - setIosEmulators( - emulators.filter( - (device) => - device.state === 'Shutdown' && - device.deviceTypeIdentifier?.match(/iPhone|iPad/i), - ), - ); - }); + getSimulators() + .then((emulators) => { + setIosEmulators( + emulators.filter( + (device) => + device.state === 'Shutdown' && + device.deviceTypeIdentifier?.match(/iPhone|iPad/i), + ), + ); + }) + .catch((e) => { + console.warn('Failed to find simulators', e); + }); }, [iosEnabled, getSimulators, store]); useEffect(() => { @@ -94,7 +94,7 @@ export const LaunchEmulatorDialog = withTrackingScope( setAndroidEmulators(emulators); }) .catch((e) => { - console.warn('Failed to find emulatiors', e); + console.warn('Failed to find emulators', e); }); }, [androidEnabled, getEmulators]); diff --git a/desktop/app/src/server/FlipperServer.tsx b/desktop/app/src/server/FlipperServer.tsx index 8f552b033..e93b0c871 100644 --- a/desktop/app/src/server/FlipperServer.tsx +++ b/desktop/app/src/server/FlipperServer.tsx @@ -24,7 +24,7 @@ import { setActiveSheet, } from '../reducers/application'; import {AndroidDeviceManager} from './devices/android/androidDeviceManager'; -import iOSDevice from './devices/ios/iOSDeviceManager'; +import {IOSDeviceManager} from './devices/ios/iOSDeviceManager'; import metroDevice from './devices/metro/metroDeviceManager'; import desktopDevice from './devices/desktop/desktopDeviceManager'; import BaseDevice from './devices/BaseDevice'; @@ -45,6 +45,9 @@ type FlipperServerEvents = { export interface FlipperServerConfig { enableAndroid: boolean; androidHome: string; + enableIOS: boolean; + idbPath: string; + enablePhysicalIOS: boolean; serverPorts: ServerPorts; } @@ -54,6 +57,9 @@ type ServerState = 'pending' | 'starting' | 'started' | 'error' | 'closed'; const defaultConfig: FlipperServerConfig = { androidHome: '', enableAndroid: false, + enableIOS: false, + enablePhysicalIOS: false, + idbPath: '', serverPorts: { insecure: -1, secure: -1, @@ -78,6 +84,7 @@ export class FlipperServer { private readonly devices = new Map(); state: ServerState = 'pending'; android: AndroidDeviceManager; + ios: IOSDeviceManager; // TODO: remove store argument constructor( @@ -89,6 +96,7 @@ export class FlipperServer { this.config = {...defaultConfig, ...config}; const server = (this.server = new ServerController(this)); this.android = new AndroidDeviceManager(this); + this.ios = new IOSDeviceManager(this); server.addListener('new-client', (client: Client) => { this.emit('client-connected', client); @@ -200,7 +208,7 @@ export class FlipperServer { async startDeviceListeners() { this.disposers.push( await this.android.watchAndroidDevices(), - iOSDevice(this.store, this.logger), + await this.ios.watchIOSDevices(), metroDevice(this), desktopDevice(this), ); @@ -271,6 +279,10 @@ export class FlipperServer { return Array.from(this.devices.keys()); } + getDevices(): BaseDevice[] { + return Array.from(this.devices.values()); + } + public async close() { this.server.close(); for (const device of this.devices.values()) { diff --git a/desktop/app/src/server/devices/android/androidDeviceManager.tsx b/desktop/app/src/server/devices/android/androidDeviceManager.tsx index 397189514..c7f8e187d 100644 --- a/desktop/app/src/server/devices/android/androidDeviceManager.tsx +++ b/desktop/app/src/server/devices/android/androidDeviceManager.tsx @@ -10,7 +10,6 @@ import AndroidDevice from './AndroidDevice'; import KaiOSDevice from './KaiOSDevice'; import child_process from 'child_process'; -import BaseDevice from '../BaseDevice'; import {getAdbClient} from './adbClient'; import which from 'which'; import {promisify} from 'util'; @@ -22,11 +21,10 @@ import {notNull} from '../../utils/typeUtils'; export class AndroidDeviceManager { // cache emulator path private emulatorPath: string | undefined; - private devices: Map = new Map(); constructor(public flipperServer: FlipperServer) {} - createDevice( + private createDevice( adbClient: ADBClient, device: any, ): Promise { @@ -106,15 +104,6 @@ export class AndroidDeviceManager { }); } - async getActiveAndroidDevices(): Promise> { - const client = await getAdbClient(this.flipperServer.config); - const androidDevices = await client.listDevices(); - const devices = await Promise.all( - androidDevices.map((device) => this.createDevice(client, device)), - ); - return devices.filter(Boolean) as any; - } - async getEmulatorPath(): Promise { if (this.emulatorPath) { return this.emulatorPath; @@ -154,7 +143,9 @@ export class AndroidDeviceManager { }); } - async getRunningEmulatorName(id: string): Promise { + private async getRunningEmulatorName( + id: string, + ): Promise { return new Promise((resolve, reject) => { const port = id.replace('emulator-', ''); // The GNU version of netcat doesn't terminate after 1s when @@ -184,8 +175,13 @@ export class AndroidDeviceManager { .then((tracker) => { tracker.on('error', (err) => { if (err.message === 'Connection closed') { - this.unregisterDevices(Array.from(this.devices.keys())); console.warn('adb server was shutdown'); + this.flipperServer + .getDevices() + .filter((d) => d instanceof AndroidDevice) + .forEach((d) => { + this.flipperServer.unregisterDevice(d.serial); + }); setTimeout(() => { this.watchAndroidDevices(); }, 500); @@ -202,14 +198,14 @@ export class AndroidDeviceManager { tracker.on('change', async (device) => { if (device.type === 'offline') { - this.unregisterDevices([device.id]); + this.flipperServer.unregisterDevice(device.id); } else { this.registerDevice(client, device); } }); tracker.on('remove', (device) => { - this.unregisterDevices([device.id]); + this.flipperServer.unregisterDevice(device.id); }); }) .catch((err: {code: string}) => { @@ -224,7 +220,7 @@ export class AndroidDeviceManager { } } - async registerDevice(adbClient: ADBClient, deviceData: any) { + private async registerDevice(adbClient: ADBClient, deviceData: any) { const androidDevice = await this.createDevice(adbClient, deviceData); if (!androidDevice) { return; @@ -232,10 +228,4 @@ export class AndroidDeviceManager { this.flipperServer.registerDevice(androidDevice); } - - unregisterDevices(serials: Array) { - serials.forEach((serial) => { - this.flipperServer.unregisterDevice(serial); - }); - } } diff --git a/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx b/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx index 66abe6b51..6015663d5 100644 --- a/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx +++ b/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx @@ -7,14 +7,12 @@ * @format */ -import { - parseXcodeFromCoreSimPath, - getAllPromisesForQueryingDevices, -} from '../iOSDeviceManager'; +import {parseXcodeFromCoreSimPath} from '../iOSDeviceManager'; import configureStore from 'redux-mock-store'; import {State, createRootReducer} from '../../../../reducers/index'; import {getInstance} from '../../../../fb-stubs/Logger'; import {IOSBridge} from '../IOSBridge'; +import {FlipperServer} from '../../../FlipperServer'; const mockStore = configureStore([])( createRootReducer()(undefined, {type: 'INIT'}), @@ -62,21 +60,15 @@ test('test parseXcodeFromCoreSimPath from standard locations', () => { }); test('test getAllPromisesForQueryingDevices when xcode detected', () => { - const promises = getAllPromisesForQueryingDevices( - mockStore, - logger, - {} as IOSBridge, - true, - ); + const flipperServer = new FlipperServer({}, mockStore, getInstance()); + flipperServer.ios.iosBridge = {} as IOSBridge; + const promises = flipperServer.ios.getAllPromisesForQueryingDevices(true); expect(promises.length).toEqual(3); }); test('test getAllPromisesForQueryingDevices when xcode is not detected', () => { - const promises = getAllPromisesForQueryingDevices( - mockStore, - logger, - {} as IOSBridge, - false, - ); + const flipperServer = new FlipperServer({}, mockStore, getInstance()); + flipperServer.ios.iosBridge = {} as IOSBridge; + const promises = flipperServer.ios.getAllPromisesForQueryingDevices(false); expect(promises.length).toEqual(1); }); diff --git a/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx b/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx index 728e48f66..3b9916a19 100644 --- a/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx +++ b/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx @@ -8,9 +8,6 @@ */ import {ChildProcess} from 'child_process'; -import {Store} from '../../../reducers/index'; -import {setXcodeDetected} from '../../../reducers/application'; -import {Logger} from '../../../fb-interfaces/Logger'; import type {DeviceType} from 'flipper-plugin'; import {promisify} from 'util'; import path from 'path'; @@ -18,14 +15,13 @@ import child_process from 'child_process'; const execFile = child_process.execFile; import iosUtil from './iOSContainerUtility'; import IOSDevice from './IOSDevice'; -import {addErrorNotification} from '../../../reducers/notifications'; import {getStaticPath} from '../../../utils/pathUtils'; -import {destroyDevice} from '../../../reducers/connections'; import { ERR_NO_IDB_OR_XCODE_AVAILABLE, IOSBridge, makeIOSBridge, } from './IOSBridge'; +import {FlipperServer} from '../../FlipperServer'; type iOSSimulatorDevice = { state: 'Booted' | 'Shutdown' | 'Shutting Down'; @@ -45,8 +41,6 @@ export type IOSDeviceParams = { const exec = promisify(child_process.exec); -let portForwarders: Array = []; - 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. @@ -58,142 +52,239 @@ function isAvailable(simulator: iOSSimulatorDevice): boolean { ); } -const portforwardingClient = getStaticPath( - path.join( - 'PortForwardingMacApp.app', - 'Contents', - 'MacOS', - 'PortForwardingMacApp', - ), -); +export class IOSDeviceManager { + private portForwarders: Array = []; -function forwardPort(port: number, multiplexChannelPort: number) { - const childProcess = execFile( - portforwardingClient, - [`-portForward=${port}`, `-multiplexChannelPort=${multiplexChannelPort}`], - (err, stdout, stderr) => { - // This happens on app reloads and doesn't need to be treated as an error. - console.warn('Port forwarding app failed to start', err, stdout, stderr); - }, - ); - console.log('Port forwarding app started', childProcess); - childProcess.addListener('error', (err) => - console.warn('Port forwarding app error', err), - ); - childProcess.addListener('exit', (code) => - console.log(`Port forwarding app exited with code ${code}`), - ); - return childProcess; -} - -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()); - }); -} - -export function getAllPromisesForQueryingDevices( - store: Store, - logger: Logger, - iosBridge: IOSBridge, - isXcodeDetected: boolean, -): Array> { - const promArray = [ - getActiveDevices( - store.getState().settingsState.idbPath, - store.getState().settingsState.enablePhysicalIOS, - ).then((devices: IOSDeviceParams[]) => { - console.log('Active iOS devices:', devices); - 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 { - const isXcodeInstalled = await iosUtil.isXcodeDetected(); - return Promise.all( - getAllPromisesForQueryingDevices( - store, - logger, - iosBridge, - isXcodeInstalled, + private portforwardingClient = getStaticPath( + path.join( + 'PortForwardingMacApp.app', + 'Contents', + 'MacOS', + 'PortForwardingMacApp', ), ); -} + iosBridge: IOSBridge | undefined; + private xcodeVersionMismatchFound = false; + public xcodeCommandLineToolsDetected = false; -function processDevices( - store: Store, - logger: Logger, - iosBridge: IOSBridge, - activeDevices: IOSDeviceParams[], - type: 'physical' | 'emulator', -) { - const {connections} = store.getState(); - const currentDeviceIDs: Set = new Set( - connections.devices - .filter( - (device) => - device instanceof IOSDevice && - device.deviceType === type && - device.connected.get(), - ) - .map((device) => device.serial), - ); - - for (const {udid, type, name} of activeDevices) { - if (currentDeviceIDs.has(udid)) { - currentDeviceIDs.delete(udid); - } else { - // clean up offline device - destroyDevice(store, logger, udid); - logger.track('usage', 'register-device', { - os: 'iOS', - type: type, - name: name, - serial: udid, - }); - const iOSDevice = new IOSDevice(iosBridge, udid, type, name); - iOSDevice.loadDevicePlugins( - store.getState().plugins.devicePlugins, - store.getState().connections.enabledDevicePlugins, - ); - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: iOSDevice, + constructor(private flipperServer: FlipperServer) { + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', () => { + this.portForwarders.forEach((process) => process.kill()); }); } } - currentDeviceIDs.forEach((id) => { - const device = store - .getState() - .connections.devices.find((device) => device.serial === id); - device?.disconnect(); - }); + private forwardPort(port: number, multiplexChannelPort: number) { + const childProcess = execFile( + this.portforwardingClient, + [`-portForward=${port}`, `-multiplexChannelPort=${multiplexChannelPort}`], + (err, stdout, stderr) => { + // This happens on app reloads and doesn't need to be treated as an error. + console.warn( + 'Port forwarding app failed to start', + err, + stdout, + stderr, + ); + }, + ); + console.log('Port forwarding app started', childProcess); + childProcess.addListener('error', (err) => + console.warn('Port forwarding app error', err), + ); + childProcess.addListener('exit', (code) => + console.log(`Port forwarding app exited with code ${code}`), + ); + return childProcess; + } + + private startDevicePortForwarders(): void { + if (this.portForwarders.length > 0) { + // Only ever start them once. + return; + } + // start port forwarding server for real device connections + // TODO: ports should be picked up from flipperServer.config? + this.portForwarders = [ + this.forwardPort(8089, 8079), + this.forwardPort(8088, 8078), + ]; + } + + getAllPromisesForQueryingDevices( + isXcodeDetected: boolean, + ): Array> { + const {config} = this.flipperServer; + const promArray = [ + getActiveDevices(config.idbPath, config.enablePhysicalIOS).then( + (devices: IOSDeviceParams[]) => { + this.processDevices(devices, 'physical'); + }, + ), + ]; + if (isXcodeDetected) { + promArray.push( + ...[ + this.checkXcodeVersionMismatch(), + this.getSimulators(true).then((devices) => { + this.processDevices(devices, 'emulator'); + }), + ], + ); + } + return promArray; + } + + private async queryDevices(): Promise { + const isXcodeInstalled = await iosUtil.isXcodeDetected(); + return Promise.all(this.getAllPromisesForQueryingDevices(isXcodeInstalled)); + } + + private processDevices( + activeDevices: IOSDeviceParams[], + type: 'physical' | 'emulator', + ) { + if (!this.iosBridge) { + throw new Error('iOS bridge not yet initialized'); + } + const currentDeviceIDs = new Set( + this.flipperServer + .getDevices() + .filter( + (device) => + device instanceof IOSDevice && + device.deviceType === type && + device.connected.get(), + ) + .map((device) => device.serial), + ); + + for (const {udid, type, name} of activeDevices) { + if (currentDeviceIDs.has(udid)) { + currentDeviceIDs.delete(udid); + } else { + const iOSDevice = new IOSDevice(this.iosBridge, udid, type, name); + this.flipperServer.registerDevice(iOSDevice); + } + } + + currentDeviceIDs.forEach((id) => { + this.flipperServer.unregisterDevice(id); + }); + } + + public async watchIOSDevices() { + // TODO: pull this condition up + if (!this.flipperServer.config.enableIOS) { + return; + } + try { + const isDetected = await iosUtil.isXcodeDetected(); + this.xcodeCommandLineToolsDetected = isDetected; + if (this.flipperServer.config.enablePhysicalIOS) { + this.startDevicePortForwarders(); + } + try { + // Awaiting the promise here to trigger immediate error handling. + this.iosBridge = await makeIOSBridge( + this.flipperServer.config.idbPath, + isDetected, + ); + this.queryDevicesForever(); + } catch (err) { + // This case is expected if both Xcode and idb are missing. + if (err.message === ERR_NO_IDB_OR_XCODE_AVAILABLE) { + console.warn( + 'Failed to init iOS device. You may want to disable iOS support in the settings.', + err, + ); + } else { + console.error('Failed to initialize iOS dispatcher:', err); + } + } + } catch (err) { + console.error('Error while querying iOS devices:', err); + } + } + + getSimulators(bootedOnly: boolean): Promise> { + return promisify(execFile)( + 'xcrun', + ['simctl', ...getDeviceSetPath(), 'list', 'devices', '--json'], + { + encoding: 'utf8', + }, + ) + .then(({stdout}) => JSON.parse(stdout).devices) + .then((simulatorDevices: Array) => { + const simulators = Object.values(simulatorDevices).flat(); + return simulators + .filter( + (simulator) => + (!bootedOnly || simulator.state === 'Booted') && + isAvailable(simulator), + ) + .map((simulator) => { + return { + ...simulator, + type: 'emulator', + } as IOSDeviceParams; + }); + }) + .catch((e: Error) => { + console.warn('Failed to query simulators:', e); + if (e.message.includes('Xcode license agreements')) { + this.flipperServer.emit('notification', { + type: 'error', + title: 'Xcode license requires approval', + description: + 'The Xcode license agreement has changed. You need to either open Xcode and agree to the terms or run `sudo xcodebuild -license` in a Terminal to allow simulators to work with Flipper.', + }); + } + return Promise.resolve([]); + }); + } + + private queryDevicesForever() { + return this.queryDevices() + .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(() => this.queryDevicesForever(), 3000); + }) + .catch((err) => { + console.warn('Failed to continuously query devices:', err); + }); + } + + async checkXcodeVersionMismatch() { + if (this.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 = parseXcodeFromCoreSimPath(line); + 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. For example: "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"`; + this.flipperServer.emit('notification', { + type: 'error', + title: 'Xcode version mismatch', + description: '' + errorMessage, + }); + this.xcodeVersionMismatchFound = true; + break; + } + } + } catch (e) { + console.error('Failed to determine Xcode version:', e); + } + } } function getDeviceSetPath() { @@ -202,47 +293,6 @@ function getDeviceSetPath() { : []; } -export function getSimulators( - store: Store, - bootedOnly: boolean, -): Promise> { - return promisify(execFile)( - 'xcrun', - ['simctl', ...getDeviceSetPath(), 'list', 'devices', '--json'], - { - encoding: 'utf8', - }, - ) - .then(({stdout}) => JSON.parse(stdout).devices) - .then((simulatorDevices: Array) => { - const simulators = Object.values(simulatorDevices).flat(); - return simulators - .filter( - (simulator) => - (!bootedOnly || simulator.state === 'Booted') && - isAvailable(simulator), - ) - .map((simulator) => { - return { - ...simulator, - type: 'emulator', - } as IOSDeviceParams; - }); - }) - .catch((e: Error) => { - console.warn('Failed to query simulators:', e); - if (e.message.includes('Xcode license agreements')) { - store.dispatch( - addErrorNotification( - 'Xcode license requires approval', - 'The Xcode license agreement has changed. You need to either open Xcode and agree to the terms or run `sudo xcodebuild -license` in a Terminal to allow simulators to work with Flipper.', - ), - ); - } - return Promise.resolve([]); - }); -} - export async function launchSimulator(udid: string): Promise { await promisify(execFile)( 'xcrun', @@ -262,88 +312,8 @@ function getActiveDevices( }); } -function queryDevicesForever( - store: Store, - logger: Logger, - iosBridge: IOSBridge, -) { - return queryDevices(store, logger, iosBridge) - .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, iosBridge), 3000); - }) - .catch((err) => { - console.warn('Failed to continuously query devices:', err); - }); -} - export function parseXcodeFromCoreSimPath( line: string, ): RegExpMatchArray | null { return line.match(/\/[\/\w@)(\-\+]*\/Xcode[^/]*\.app\/Contents\/Developer/); } - -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 = parseXcodeFromCoreSimPath(line); - 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. For example: "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"`; - store.dispatch( - addErrorNotification('Xcode version mismatch', errorMessage), - ); - xcodeVersionMismatchFound = true; - break; - } - } - } catch (e) { - console.error('Failed to determine Xcode version:', e); - } -} - -export default (store: Store, logger: Logger) => { - if (!store.getState().settingsState.enableIOS) { - return; - } - iosUtil - .isXcodeDetected() - .then(async (isDetected) => { - store.dispatch(setXcodeDetected(isDetected)); - if (store.getState().settingsState.enablePhysicalIOS) { - startDevicePortForwarders(); - } - try { - // Awaiting the promise here to trigger immediate error handling. - return await makeIOSBridge( - store.getState().settingsState.idbPath, - isDetected, - ); - } catch (err) { - // This case is expected if both Xcode and idb are missing. - if (err.message === ERR_NO_IDB_OR_XCODE_AVAILABLE) { - console.warn( - 'Failed to init iOS device. You may want to disable iOS support in the settings.', - err, - ); - } else { - console.error('Failed to initialize iOS dispatcher:', err); - } - } - }) - .then( - (iosBridge) => iosBridge && queryDevicesForever(store, logger, iosBridge), - ) - .catch((err) => { - console.error('Error while querying iOS devices:', err); - }); -}; diff --git a/desktop/app/src/server/utils/CertificateProvider.tsx b/desktop/app/src/server/utils/CertificateProvider.tsx index 4973e762e..9e3755c43 100644 --- a/desktop/app/src/server/utils/CertificateProvider.tsx +++ b/desktop/app/src/server/utils/CertificateProvider.tsx @@ -29,6 +29,7 @@ import {Client as ADBClient} from 'adbkit'; import archiver from 'archiver'; import {timeout} from 'flipper-plugin'; import {v4 as uuid} from 'uuid'; +import {isTest} from '../../utils/isProduction'; export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW'; @@ -581,7 +582,10 @@ export default class CertificateProvider { }); } - ensureCertificateAuthorityExists(): Promise { + async ensureCertificateAuthorityExists(): Promise { + if (isTest()) { + return; + } if (!fs.existsSync(caKey)) { return this.generateCertificateAuthority(); }