diff --git a/desktop/app/src/dispatcher/iOSDevice.tsx b/desktop/app/src/dispatcher/iOSDevice.tsx index 4e0e1f287..1222b0690 100644 --- a/desktop/app/src/dispatcher/iOSDevice.tsx +++ b/desktop/app/src/dispatcher/iOSDevice.tsx @@ -16,7 +16,7 @@ import {promisify} from 'util'; import path from 'path'; import child_process from 'child_process'; const execFile = child_process.execFile; -import iosUtil from '../fb-stubs/iOSContainerUtility'; +import iosUtil from '../utils/iOSContainerUtility'; import IOSDevice from '../devices/IOSDevice'; import isProduction from '../utils/isProduction'; import GK from '../fb-stubs/GK'; diff --git a/desktop/app/src/fb-stubs/iOSContainerUtility.tsx b/desktop/app/src/fb-stubs/iOSContainerUtility.tsx deleted file mode 100644 index 004f07a99..000000000 --- a/desktop/app/src/fb-stubs/iOSContainerUtility.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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 {DeviceType} from '../devices/BaseDevice'; -import {exec} from 'promisify-child-process'; -import {notNull} from '../utils/typeUtils'; -import {killOrphanedInstrumentsProcesses} from '../utils/processCleanup'; - -const errorMessage = 'Physical iOS devices not yet supported'; - -export type DeviceTarget = { - udid: string; - type: DeviceType; - name: string; -}; - -function isAvailable(): boolean { - return false; -} - -async function targets(): Promise> { - await killOrphanedInstrumentsProcesses(); - const {stdout} = await exec('instruments -s devices'); - if (!stdout) { - return []; - } - return stdout - .toString() - .split('\n') - .map((line) => line.trim()) - .map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line)) - .filter(notNull) - .filter( - ([_match, name, _udid, isSim]) => - !isSim && (name.includes('iPhone') || name.includes('iPad')), - ) - .map(([_match, name, udid]) => { - return {udid: udid, type: 'physical', name: name}; - }); -} - -function push( - _udid: string, - _src: string, - _bundleId: string, - _dst: string, -): Promise { - return Promise.reject(errorMessage); -} - -function pull( - _udid: string, - _src: string, - _bundleId: string, - _dst: string, -): Promise { - return Promise.reject(errorMessage); -} - -export default { - isAvailable: isAvailable, - targets: targets, - push: push, - pull: pull, -}; diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 224601859..23f747a03 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -14,7 +14,7 @@ import MacDevice from '../devices/MacDevice'; import Client from '../Client'; import {UninitializedClient} from '../UninitializedClient'; import {isEqual} from 'lodash'; -import iosUtil from '../fb-stubs/iOSContainerUtility'; +import iosUtil from '../utils/iOSContainerUtility'; import {performance} from 'perf_hooks'; import isHeadless from '../utils/isHeadless'; import {Actions} from '.'; diff --git a/desktop/app/src/utils/CertificateProvider.tsx b/desktop/app/src/utils/CertificateProvider.tsx index f1a904197..afc202712 100644 --- a/desktop/app/src/utils/CertificateProvider.tsx +++ b/desktop/app/src/utils/CertificateProvider.tsx @@ -17,7 +17,7 @@ import { } from './openssl-wrapper-with-promises'; import path from 'path'; import tmp, {DirOptions, FileOptions} from 'tmp'; -import iosUtil from '../fb-stubs/iOSContainerUtility'; +import iosUtil from './iOSContainerUtility'; import {reportPlatformFailures} from './metrics'; import {getAdbClient} from './adbClient'; import * as androidUtil from './androidContainerUtility'; diff --git a/desktop/app/src/utils/iOSContainerUtility.tsx b/desktop/app/src/utils/iOSContainerUtility.tsx new file mode 100644 index 000000000..20b52bb9a --- /dev/null +++ b/desktop/app/src/utils/iOSContainerUtility.tsx @@ -0,0 +1,133 @@ +/** + * 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 child_process from 'child_process'; +import {promisify} from 'util'; +import {Mutex} from 'async-mutex'; +import {notNull} from './typeUtils'; +const unsafeExec = promisify(child_process.exec); +import {killOrphanedInstrumentsProcesses} from './processCleanup'; +import {reportPlatformFailures} from './metrics'; +import config from '../fb-stubs/config'; + +const idbPath = '/usr/local/bin/idb'; +// Use debug to get helpful logs when idb fails +const idbLogLevel = 'DEBUG'; +const operationPrefix = 'iosContainerUtility'; + +const mutex = new Mutex(); + +export type DeviceTarget = { + udid: string; + type: 'physical' | 'emulator'; + name: string; +}; + +function isAvailable(): boolean { + return config.isFBBuild; +} + +function safeExec(command: string): Promise<{stdout: string; stderr: string}> { + return mutex.acquire().then((release) => { + return unsafeExec(command).finally(release); + }); +} + +async function targets(): Promise> { + if (process.platform !== 'darwin') { + return []; + } + await killOrphanedInstrumentsProcesses(); + return safeExec('instruments -s devices').then(({stdout}) => + stdout + .toString() + .split('\n') + .map((line) => line.trim()) + .map((line) => /(.+) \([^(]+\) \[(.*)\]( \(Simulator\))?/.exec(line)) + .filter(notNull) + .filter( + ([_match, name, _udid, isSim]) => + !isSim && (name.includes('iPhone') || name.includes('iPad')), + ) + .map(([_match, name, udid]) => { + return {udid: udid, type: 'physical', name: name}; + }), + ); +} + +function push( + udid: string, + src: string, + bundleId: string, + dst: string, +): Promise { + return wrapWithErrorMessage( + reportPlatformFailures( + safeExec( + `${idbPath} --log ${idbLogLevel} file push --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, + ) + .then(() => { + return; + }) + .catch(handleMissingIdb), + `${operationPrefix}:push`, + ), + ); +} + +function pull( + udid: string, + src: string, + bundleId: string, + dst: string, +): Promise { + return wrapWithErrorMessage( + reportPlatformFailures( + safeExec( + `${idbPath} --log ${idbLogLevel} file pull --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, + ) + .then(() => { + return; + }) + .catch(handleMissingIdb), + `${operationPrefix}:pull`, + ), + ); +} + +// The 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): void { + if ( + e.message && + e.message.includes('sudo: no tty present and no askpass program specified') + ) { + throw new Error( + `idb doesn't appear to be installed. Run "${idbPath} list-targets" to fix this.`, + ); + } + throw e; +} + +function wrapWithErrorMessage(p: Promise): Promise { + return p.catch((e: Error) => { + console.error(e); + // Give the user instructions. Don't embed the error because it's unique per invocation so won't be deduped. + throw new Error( + "A problem with idb has ocurred. Please run `sudo rm -rf /tmp/idb*` and `sudo yum install -y fb-idb` to update it, if that doesn't fix it, post in Flipper Support.", + ); + }); +} + +export default { + isAvailable: isAvailable, + targets: targets, + push: push, + pull: pull, +};