diff --git a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSContainerUtility.node.tsx b/desktop/flipper-server-core/src/devices/ios/__tests__/iOSContainerUtility.node.tsx deleted file mode 100644 index bfe14483831..00000000000 --- a/desktop/flipper-server-core/src/devices/ios/__tests__/iOSContainerUtility.node.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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); -}); diff --git a/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx b/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx index 1c58eb0d9ae..9247f6b990f 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx @@ -25,8 +25,8 @@ export type IdbConfig = { }; // Use debug to get helpful logs when idb fails -const idbLogLevel = 'DEBUG'; -const operationPrefix = 'iosContainerUtility'; +const IDB_LOG_LEVEL = 'DEBUG'; +const LOG_TAG = 'iOSContainerUtility'; const mutex = new Mutex(); @@ -48,54 +48,94 @@ export type DeviceTarget = { name: string; }; -function isAvailable(idbPath: string): Promise { +async function isAvailable(idbPath: string): Promise { if (!idbPath) { - return Promise.resolve(false); + return false; } - return promises - .access(idbPath, constants.X_OK) - .then((_) => true) - .catch((_) => false); + try { + await promises.access(idbPath, constants.X_OK); + } catch (e) { + return false; + } + return true; } -function safeExec( +async function safeExec( command: string, ): Promise<{stdout: string; stderr: string} | Output> { - return mutex - .acquire() - .then((release) => unsafeExec(command).finally(release)); + const release = await mutex.acquire(); + return await unsafeExec(command).finally(release); +} + +async function queryTargetsWithXcode(): Promise> { + const cmd = 'xcrun xctrace list devices'; + try { + const {stdout} = await safeExec(cmd); + if (!stdout) { + throw new Error('No output from command'); + } + + return stdout + .toString() + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line)) + .filter(notNull) + .filter(([_match, _name, _udid, isSim]) => !isSim) + .map(([_match, name, udid]) => { + return {udid, type: 'physical', name}; + }); + } catch (e) { + console.warn(`Failed to query devices using '${cmd}'`, e); + return []; + } +} + +async function queryTargetsWithIdb( + idbPath: string, +): Promise> { + const cmd = `${idbPath} list-targets --json`; + try { + const {stdout} = await safeExec(cmd); + if (!stdout) { + throw new Error('No output from command'); + } + return parseIdbTargets(stdout.toString()); + } catch (e) { + console.warn(`Failed to execute '${cmd}' for targets.`, e); + return []; + } } -export async function queryTargetsWithoutXcodeDependency( +async function queryTargetsWithIdbCompanion( idbCompanionPath: string, isPhysicalDeviceEnabled: boolean, - isAvailableFunc: (idbPath: string) => Promise, - safeExecFunc: ( - command: string, - ) => Promise<{stdout: string; stderr: string} | Output>, ): Promise> { - if (await isAvailableFunc(idbCompanionPath)) { - return safeExecFunc(`${idbCompanionPath} --list 1 --only device`) - .then(({stdout}) => parseIdbTargets(stdout!.toString())) - .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) => { + if (await isAvailable(idbCompanionPath)) { + const cmd = `${idbCompanionPath} --list 1 --only device`; + try { + const {stdout} = await safeExec(cmd); + if (!stdout) { + throw new Error('No output from command'); + } + + const devices = parseIdbTargets(stdout.toString()); + if (devices.length > 0 && !isPhysicalDeviceEnabled) { console.warn( - 'Failed to query idb_companion --list 1 --only device for physical targets:', - e, + `You are trying to connect Physical Device. + Please enable the toggle "Enable physical iOS device" from the setting screen.`, ); - return []; - }); + } + return devices; + } catch (e) { + console.warn(`Failed to execute '${cmd}' for targets:`, e); + return []; + } } else { console.warn( - `Unable to locate idb_companion in ${idbCompanionPath}. Try running sudo yum install -y fb-idb`, + `Unable to locate idb_companion in '${idbCompanionPath}'. + Try running sudo yum install -y fb-idb`, ); return []; } @@ -125,8 +165,6 @@ function parseIdbTargets(lines: string): Array { .map((line) => parseIdbTarget(line)) .filter((target): target is DeviceTarget => !!target); - // For some reason, idb can return duplicates - // TODO: Raise the issue with idb const dedupedIdbTargets: Record = {}; for (const idbTarget of parsedIdbTargets) { dedupedIdbTargets[idbTarget.udid] = @@ -135,38 +173,20 @@ function parseIdbTargets(lines: string): Array { return Object.values(dedupedIdbTargets); } -export async function idbListTargets( +async function idbDescribeTarget( idbPath: string, - safeExecFunc: ( - command: string, - ) => Promise<{stdout: string; stderr: string} | Output> = safeExec, -): Promise> { - return safeExecFunc(`${idbPath} list-targets --json`) - .then(({stdout}) => - // See above. - parseIdbTargets(stdout!.toString()), - ) - .catch((e: Error) => { - console.warn('Failed to query idb for targets:', e); - return []; - }); -} - -export async function idbDescribeTarget( - idbPath: string, - safeExecFunc: ( - command: string, - ) => Promise<{stdout: string; stderr: string} | Output> = safeExec, ): Promise { - return safeExecFunc(`${idbPath} describe --json`) - .then(({stdout}) => - // See above. - parseIdbTarget(stdout!.toString()), - ) - .catch((e: Error) => { - console.warn('Failed to query idb to describe a target:', e); - return undefined; - }); + const cmd = `${idbPath} describe --json`; + try { + const {stdout} = await safeExec(cmd); + if (!stdout) { + throw new Error('No output from command'); + } + return parseIdbTarget(stdout.toString()); + } catch (e) { + console.warn(`Failed to execute '${cmd}' to describe a target.`, e); + return undefined; + } } async function targets( @@ -177,8 +197,9 @@ async function targets( return []; } - // If companion is started by some external process and its address provided to Flipper via IDB_COMPANION environment variable, - // use that companion and do not query other devices + // If companion is started by some external process and its path + // is provided to Flipper via IDB_COMPANION environment variable, + // use that instead and do not query other devices. // See stack of D36315576 for details if (process.env.IDB_COMPANION) { const target = await idbDescribeTarget(idbPath); @@ -190,15 +211,13 @@ async function targets( 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.', + '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 queryTargetsWithoutXcodeDependency( + return queryTargetsWithIdbCompanion( idbCompanionPath, isPhysicalDeviceEnabled, - isAvailable, - safeExec, ); } @@ -207,28 +226,10 @@ async function targets( // 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. - // TODO: Move idb availability check up. if (await memoize(isAvailable)(idbPath)) { - return await idbListTargets(idbPath); + return await queryTargetsWithIdb(idbPath); } else { - return safeExec('xcrun xctrace list devices') - .then(({stdout}) => - stdout! - .toString() - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line)) - .filter(notNull) - .filter(([_match, _name, _udid, isSim]) => !isSim) - .map(([_match, name, udid]) => { - return {udid, type: 'physical', name}; - }), - ) - .catch((e) => { - console.warn('Failed to query for devices using xctrace:', e); - return []; - }); + return queryTargetsWithXcode(); } } @@ -241,16 +242,18 @@ async function push( ): Promise { await memoize(checkIdbIsInstalled)(idbPath); - return reportPlatformFailures( - safeExec( - `${idbPath} file push --log ${idbLogLevel} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, - ) - .then(() => { - return; - }) - .catch((e) => handleMissingIdb(e, idbPath)), - `${operationPrefix}:push`, - ); + const push_ = async () => { + try { + await safeExec( + `${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, + ); + } catch (e) { + handleMissingIdb(e, idbPath); + throw e; + } + }; + + return reportPlatformFailures(push_(), `${LOG_TAG}:push`); } async function pull( @@ -262,30 +265,33 @@ async function pull( ): Promise { await memoize(checkIdbIsInstalled)(idbPath); - return reportPlatformFailures( - safeExec( - `${idbPath} file pull --log ${idbLogLevel} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, - ) - .then(() => { - return; - }) - .catch((e) => handleMissingIdb(e, idbPath)) - .catch((e) => handleMissingPermissions(e)), - `${operationPrefix}:pull`, - ); + const pull_ = async () => { + try { + await safeExec( + `${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, + ); + } catch (e) { + handleMissingIdb(e, idbPath); + handleMissingPermissions(e); + throw e; + } + }; + + return reportPlatformFailures(pull_(), `${LOG_TAG}:pull`); } -export async function checkIdbIsInstalled(idbPath: string): Promise { +async function checkIdbIsInstalled(idbPath: string): Promise { const isInstalled = await isAvailable(idbPath); if (!isInstalled) { throw new Error( - `idb is required to use iOS devices. Install it with instructions from https://github.com/facebook/idb and set the installation path in Flipper settings.`, + `idb is required to use iOS devices. Install it with instructions + from https://github.com/facebook/idb and set the installation path in Flipper settings.`, ); } } -// The fb-internal idb binary is a shim that downloads the proper one on first run. It requires sudo to do so. -// If we detect this, Tell the user how to fix it. +// The fb-internal idb binary is a shim that downloads the proper one on first run. +// It requires sudo to do so. If we detect this, tell the user how to fix it. function handleMissingIdb(e: Error, idbPath: string): void { if ( e.message && @@ -296,7 +302,6 @@ function handleMissingIdb(e: Error, idbPath: string): void { `idb doesn't appear to be installed. Run "${idbPath} list-targets" to fix this.`, ); } - throw e; } function handleMissingPermissions(e: Error): void { @@ -309,22 +314,21 @@ function handleMissingPermissions(e: Error): void { console.warn(e); throw new Error( 'Cannot connect to iOS application. idb_certificate_pull_failed' + - 'Idb lacks permissions to exchange certificates. Did you install a source build ([FB] or enable certificate exchange)? See console logs for more details.', + 'idb lacks permissions to exchange certificates. Did you install a source build ([FB] or enable certificate exchange)? See console logs for more details.', ); } - throw e; } async function isXcodeDetected(): Promise { - return exec('xcode-select -p') - .then(({stdout}) => { - return fs.pathExists(stdout.trim()); - }) - .catch((_) => false); + try { + const {stdout} = await exec('xcode-select -p'); + return fs.pathExists(stdout.trim()); + } catch (e) { + return false; + } } export default { - isAvailable, targets, push, pull,