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
This commit is contained in:
Luke De Feo
2022-07-07 07:50:14 -07:00
committed by Facebook GitHub Bot
parent 1b02f105dc
commit 6c5faf2932
11 changed files with 99 additions and 1 deletions

View File

@@ -223,6 +223,10 @@ export type FlipperServerCommands = {
) => Promise<void>;
'device-stop-screencapture': (serial: string) => Promise<string>; // file path
'device-shell-exec': (serial: string, command: string) => Promise<string>;
'device-install-app': (
serial: string,
appBundlePath: string,
) => Promise<void>;
'device-forward-port': (
serial: string,
local: string,

View File

@@ -235,6 +235,14 @@ export default class BaseDevice implements Device {
return this.flipperServer.exec('device-navigate', this.serial, location);
}
async installApp(appBundlePath: string): Promise<void> {
return this.flipperServer.exec(
'device-install-app',
this.serial,
appBundlePath,
);
}
async screenshot(): Promise<Uint8Array | undefined> {
if (!this.description.features.screenshotAvailable || this.isArchived) {
return;

View File

@@ -42,6 +42,7 @@ export interface Device {
sendMetroCommand(command: string): Promise<void>;
navigateToLocation(location: string): Promise<void>;
screenshot(): Promise<Uint8Array | undefined>;
installApp(appBundlePath: string): Promise<void>;
}
export type DevicePluginPredicate = (device: Device) => boolean;

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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<void> {
throw new Error('Install not implemented');
}
}

View File

@@ -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(

View File

@@ -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<Array<IOSDeviceParams>>;
installApp: (
serial: string,
ipaPath: string,
tempPath: string,
) => Promise<void>;
}
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<void> {
console.log(`Installing app via IDB ${ipaPath} ${serial}`);
await this._execIdb(`install ${ipaPath} --udid ${serial}`);
}
async getActiveDevices(_bootedOnly: boolean): Promise<IOSDeviceParams[]> {
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<void> {
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<void> {
//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<Buffer> {
const buffer = await fs.readFile(imagePath);
await fs.unlink(imagePath);

View File

@@ -120,6 +120,14 @@ export default class IOSDevice extends ServerDevice {
return output;
}
async installApp(ipaPath: string): Promise<void> {
return this.iOSBridge.installApp(
this.serial,
ipaPath,
this.flipperServer.config.paths.tempPath,
);
}
disconnect() {
if (this.recording) {
this.stopScreenCapture();

View File

@@ -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',
);
});

View File

@@ -75,6 +75,8 @@ declare module 'adbkit' {
local: string,
remote: string,
) => Promise<boolean>; // TODO: verify correctness of signature
install: (serial: string, apkPath: string) => Promise<boolean>;
}
export function createClient(config: {port: number; host: string}): Client;
}