From 6c5faf2932a9ffec7cf1d7550e8ac8ff340ad28f Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Thu, 7 Jul 2022 07:50:14 -0700 Subject: [PATCH] Add command to install app to flipper server Summary: There is a new flipper server command to install apps. For android it uses adb (via adb kit) For ios depending on idb availablity it will use idb or xcrun. Consumed in the next diff Reviewed By: lblasa, aigoncharov Differential Revision: D36936637 fbshipit-source-id: e09d34d840a9f3bf9136bcaf94fb8ca15dd27cbb --- desktop/flipper-common/src/server-types.tsx | 4 ++ .../src/devices/BaseDevice.tsx | 8 ++++ .../src/plugin/DevicePlugin.tsx | 1 + .../src/test-utils/test-utils.tsx | 3 ++ .../src/FlipperServerImpl.tsx | 3 ++ .../src/devices/ServerDevice.tsx | 4 ++ .../src/devices/android/AndroidDevice.tsx | 5 ++ .../src/devices/ios/IOSBridge.tsx | 47 ++++++++++++++++++- .../src/devices/ios/IOSDevice.tsx | 8 ++++ .../devices/ios/__tests__/IOSBridge.node.tsx | 15 ++++++ desktop/types/adbkit.d.ts | 2 + 11 files changed, 99 insertions(+), 1 deletion(-) diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index 2472828f6..53725bc93 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -223,6 +223,10 @@ export type FlipperServerCommands = { ) => Promise; 'device-stop-screencapture': (serial: string) => Promise; // file path 'device-shell-exec': (serial: string, command: string) => Promise; + 'device-install-app': ( + serial: string, + appBundlePath: string, + ) => Promise; 'device-forward-port': ( serial: string, local: string, diff --git a/desktop/flipper-frontend-core/src/devices/BaseDevice.tsx b/desktop/flipper-frontend-core/src/devices/BaseDevice.tsx index a393c16a2..9f88fe905 100644 --- a/desktop/flipper-frontend-core/src/devices/BaseDevice.tsx +++ b/desktop/flipper-frontend-core/src/devices/BaseDevice.tsx @@ -235,6 +235,14 @@ export default class BaseDevice implements Device { return this.flipperServer.exec('device-navigate', this.serial, location); } + async installApp(appBundlePath: string): Promise { + return this.flipperServer.exec( + 'device-install-app', + this.serial, + appBundlePath, + ); + } + async screenshot(): Promise { if (!this.description.features.screenshotAvailable || this.isArchived) { return; diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index db6939fe1..0e1b03f0d 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -42,6 +42,7 @@ export interface Device { sendMetroCommand(command: string): Promise; navigateToLocation(location: string): Promise; screenshot(): Promise; + installApp(appBundlePath: string): Promise; } export type DevicePluginPredicate = (device: Device) => boolean; diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index c1e6827a1..327c2dc7b 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -602,6 +602,9 @@ function createMockDevice(options?: StartPluginOptions): Device & { get isConnected() { return this.connected.get(); }, + installApp(_: string) { + return Promise.resolve(); + }, navigateToLocation: createStubFunction(), screenshot: createStubFunction(), sendMetroCommand: createStubFunction(), diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index e94d70f23..09e810e45 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -285,6 +285,9 @@ export class FlipperServerImpl implements FlipperServer { } private commandHandler: FlipperServerCommands = { + 'device-install-app': async (serial, bundlePath) => { + return this.devices.get(serial)?.installApp(bundlePath); + }, 'get-server-state': async () => ({ state: this.state, error: this.stateError, diff --git a/desktop/flipper-server-core/src/devices/ServerDevice.tsx b/desktop/flipper-server-core/src/devices/ServerDevice.tsx index 2c9bcdc40..89dd1439e 100644 --- a/desktop/flipper-server-core/src/devices/ServerDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ServerDevice.tsx @@ -80,4 +80,8 @@ export abstract class ServerDevice { async navigateToLocation(_location: string) { throw new Error('navigateLocation not implemented on BaseDevice'); } + + async installApp(_appBundlePath: string): Promise { + throw new Error('Install not implemented'); + } } diff --git a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx b/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx index 00d895a4a..163ef0d64 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidDevice.tsx @@ -275,6 +275,11 @@ export default class AndroidDevice extends ServerDevice { } super.disconnect(); } + + async installApp(apkPath: string) { + console.log(`Installing app with adb ${apkPath}`); + await this.adb.install(this.serial, apkPath); + } } export async function launchEmulator( diff --git a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx index 97806af0c..a8269247c 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx @@ -16,7 +16,6 @@ import {DeviceType, uuid} from 'flipper-common'; import path from 'path'; import {exec, execFile} from 'promisify-child-process'; import {getFlipperServerConfig} from '../../FlipperServerConfig'; - export const ERR_NO_IDB_OR_XCODE_AVAILABLE = 'Neither Xcode nor idb available. Cannot provide iOS device functionality.'; @@ -44,6 +43,11 @@ export interface IOSBridge { outputFile: string, ) => child_process.ChildProcess; getActiveDevices: (bootedOnly: boolean) => Promise>; + installApp: ( + serial: string, + ipaPath: string, + tempPath: string, + ) => Promise; } export class IDBBridge implements IOSBridge { @@ -51,6 +55,12 @@ export class IDBBridge implements IOSBridge { private idbPath: string, private enablePhysicalDevices: boolean, ) {} + + async installApp(serial: string, ipaPath: string): Promise { + console.log(`Installing app via IDB ${ipaPath} ${serial}`); + await this._execIdb(`install ${ipaPath} --udid ${serial}`); + } + async getActiveDevices(_bootedOnly: boolean): Promise { return iosUtil .targets(this.idbPath, this.enablePhysicalDevices) @@ -96,6 +106,31 @@ export class IDBBridge implements IOSBridge { } export class SimctlBridge implements IOSBridge { + async installApp( + serial: string, + ipaPath: string, + tempPath: string, + ): Promise { + console.log(`Installing app ${ipaPath} with xcrun`); + const buildName = path.parse(ipaPath).name; + + const extractTmpDir = path.join(tempPath, `${buildName}-extract`, uuid()); + + try { + await fs.mkdirp(extractTmpDir); + await unzip(ipaPath, extractTmpDir); + await exec( + `xcrun simctl install ${serial} ${path.join( + extractTmpDir, + 'Payload', + '*.app', + )}`, + ); + } finally { + await fs.rmdir(extractTmpDir, {recursive: true}); + } + } + startLogListener( udid: string, deviceType: DeviceType, @@ -214,6 +249,16 @@ function makeTempScreenshotFilePath() { return path.join(getFlipperServerConfig().paths.tempPath, imageName); } +async function unzip(filePath: string, destination: string): Promise { + //todo this probably shouldn't involve shelling out... + await exec(`unzip -qq -o ${filePath} -d ${destination}`); + if (!(await fs.pathExists(path.join(destination, 'Payload')))) { + throw new Error( + `${path.join(destination, 'Payload')} Directory does not exists`, + ); + } +} + async function readScreenshotIntoBuffer(imagePath: string): Promise { const buffer = await fs.readFile(imagePath); await fs.unlink(imagePath); diff --git a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx index 06c6e2d75..0123a8dfc 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx @@ -120,6 +120,14 @@ export default class IOSDevice extends ServerDevice { return output; } + async installApp(ipaPath: string): Promise { + return this.iOSBridge.installApp( + this.serial, + ipaPath, + this.flipperServer.config.paths.tempPath, + ); + } + disconnect() { if (this.recording) { this.stopScreenCapture(); diff --git a/desktop/flipper-server-core/src/devices/ios/__tests__/IOSBridge.node.tsx b/desktop/flipper-server-core/src/devices/ios/__tests__/IOSBridge.node.tsx index 1635abed8..dba9384af 100644 --- a/desktop/flipper-server-core/src/devices/ios/__tests__/IOSBridge.node.tsx +++ b/desktop/flipper-server-core/src/devices/ios/__tests__/IOSBridge.node.tsx @@ -196,3 +196,18 @@ test('uses idb to record when available', async () => { '/usr/local/bin/idb record-video --udid deadbeef /tmo/video.mp4', ); }); + +test('uses idb to install when available', async () => { + const ib = await makeIOSBridge( + '/usr/local/bin/idb', + true, + true, + async (_) => true, + ); + + await ib.installApp('sim1', '/tmp/foo/bar.zip', '/tmp/hello'); + + expect(promisifyChildProcess.exec).toHaveBeenCalledWith( + '/usr/local/bin/idb install /tmp/foo/bar.zip --udid sim1', + ); +}); diff --git a/desktop/types/adbkit.d.ts b/desktop/types/adbkit.d.ts index 42b0b1306..571bc112d 100644 --- a/desktop/types/adbkit.d.ts +++ b/desktop/types/adbkit.d.ts @@ -75,6 +75,8 @@ declare module 'adbkit' { local: string, remote: string, ) => Promise; // TODO: verify correctness of signature + + install: (serial: string, apkPath: string) => Promise; } export function createClient(config: {port: number; host: string}): Client; }