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:
committed by
Facebook GitHub Bot
parent
1b02f105dc
commit
6c5faf2932
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
2
desktop/types/adbkit.d.ts
vendored
2
desktop/types/adbkit.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user