Get files from Flipper folder for every app on an iOS device

Summary:
Design doc: https://docs.google.com/document/d/1HLCFl46RfqG0o1mSt8SWrwf_HMfOCRg_oENioc1rkvQ/edit#

Fetch list of files in the `/sonar` folder on iOS devices and fetch all the files

Reviewed By: passy

Differential Revision: D40548410

fbshipit-source-id: d38cbbb1e3b5579c13f30777233e3caf7b8c9b34
This commit is contained in:
Andrey Goncharov
2022-10-25 05:31:48 -07:00
committed by Facebook GitHub Bot
parent 3c8ebf105a
commit 821bf2b5b7
3 changed files with 228 additions and 5 deletions

View File

@@ -14,8 +14,9 @@ import child_process from 'child_process';
import type {IOSDeviceParams} from 'flipper-common';
import {DeviceType, uuid} from 'flipper-common';
import path from 'path';
import {exec, execFile} from 'promisify-child-process';
import {ChildProcessPromise, exec, execFile} from 'promisify-child-process';
import {getFlipperServerConfig} from '../../FlipperServerConfig';
import iOSContainerUtility from './iOSContainerUtility';
export const ERR_NO_IDB_OR_XCODE_AVAILABLE =
'Neither Xcode nor idb available. Cannot provide iOS device functionality.';
@@ -31,6 +32,16 @@ type iOSSimulatorDevice = {
udid: string;
};
// https://fbidb.io/docs/commands#list-apps
interface IOSInstalledAppDescriptor {
bundleID: string;
name: string;
installType: 'user' | 'user_development' | 'system';
architectures: string[];
runningStatus: 'Unknown' | 'Running';
debuggableStatus: boolean;
}
export interface IOSBridge {
startLogListener: (
udid: string,
@@ -48,6 +59,14 @@ export interface IOSBridge {
ipaPath: string,
tempPath: string,
) => Promise<void>;
getInstalledApps: (serial: string) => Promise<IOSInstalledAppDescriptor[]>;
ls: (serial: string, appBundleId: string, path: string) => Promise<string[]>;
pull: (
serial: string,
src: string,
bundleId: string,
dst: string,
) => Promise<void>;
}
export class IDBBridge implements IOSBridge {
@@ -56,6 +75,66 @@ export class IDBBridge implements IOSBridge {
private enablePhysicalDevices: boolean,
) {}
async getInstalledApps(serial: string): Promise<IOSInstalledAppDescriptor[]> {
const {stdout} = await this._execIdb(`list-apps --udid ${serial}`);
if (typeof stdout !== 'string') {
throw new Error(
`IDBBridge.getInstalledApps -> returned ${typeof stdout}, not a string`,
);
}
// Skip last item, as the last line also has \n at the end
const appStrings = stdout.split('\n').slice(0, -1);
const appDescriptors = appStrings.map(
(appString): IOSInstalledAppDescriptor => {
const [
bundleID,
name,
installType,
architecturesString,
runningStatus,
debuggableStatusString,
] = appString.split(' | ');
return {
bundleID,
name,
installType: installType as IOSInstalledAppDescriptor['installType'],
architectures: architecturesString.split(', '),
runningStatus:
runningStatus as IOSInstalledAppDescriptor['runningStatus'],
debuggableStatus: debuggableStatusString !== 'Not Debuggable',
};
},
);
return appDescriptors;
}
async ls(
serial: string,
appBundleId: string,
path: string,
): Promise<string[]> {
const {stdout} = await this._execIdb(
`file ls --udid ${serial} --log ERROR --bundle-id ${appBundleId} '${path}'`,
);
if (typeof stdout !== 'string') {
throw new Error(
`IDBBridge.ls -> returned ${typeof stdout}, not a string`,
);
}
// Skip last item, as the last line also has \n at the end
const pathContent = stdout.split('\n').slice(0, -1);
return pathContent;
}
pull(
serial: string,
src: string,
bundleId: string,
dst: string,
): Promise<void> {
return iOSContainerUtility.pull(serial, src, bundleId, dst, this.idbPath);
}
async installApp(serial: string, ipaPath: string): Promise<void> {
console.log(`Installing app via IDB ${ipaPath} ${serial}`);
await this._execIdb(`install ${ipaPath} --udid ${serial}`);
@@ -100,12 +179,37 @@ export class IDBBridge implements IOSBridge {
);
}
_execIdb(command: string): child_process.ChildProcess {
_execIdb(command: string): ChildProcessPromise {
return exec(`${this.idbPath} ${command}`);
}
}
export class SimctlBridge implements IOSBridge {
pull(
serial: string,
src: string,
bundleId: string,
dst: string,
): Promise<void> {
return iOSContainerUtility.pull(serial, src, bundleId, dst, '');
}
async getInstalledApps(
_serial: string,
): Promise<IOSInstalledAppDescriptor[]> {
// TODO: Implement me
throw new Error(
'SimctlBridge does not support getInstalledApps. Install IDB (https://fbidb.io/).',
);
}
async ls(_serial: string, _appBundleId: string): Promise<string[]> {
// TODO: Implement me
throw new Error(
'SimctlBridge does not support ls. Install IDB (https://fbidb.io/).',
);
}
async installApp(
serial: string,
ipaPath: string,

View File

@@ -7,15 +7,25 @@
* @format
*/
import {DeviceType, timeout} from 'flipper-common';
import {DeviceDebugData, DeviceType, timeout} from 'flipper-common';
import {ChildProcess} from 'child_process';
import {IOSBridge} from './IOSBridge';
import {ServerDevice} from '../ServerDevice';
import {FlipperServerImpl} from '../../FlipperServerImpl';
import {iOSCrashWatcher} from './iOSCrashUtils';
import {iOSLogListener} from './iOSLogListener';
import {DebuggableDevice} from '../DebuggableDevice';
import tmp, {DirOptions} from 'tmp';
import {promisify} from 'util';
import path from 'path';
import {readFile} from 'fs/promises';
export default class IOSDevice extends ServerDevice {
const tmpDir = promisify(tmp.dir) as (options?: DirOptions) => Promise<string>;
export default class IOSDevice
extends ServerDevice
implements DebuggableDevice
{
private recording?: {process: ChildProcess; destination: string};
private iOSBridge: IOSBridge;
readonly logListener: iOSLogListener;
@@ -128,6 +138,111 @@ export default class IOSDevice extends ServerDevice {
);
}
async readFlipperFolderForAllApps(): Promise<DeviceDebugData[]> {
console.debug('IOSDevice.readFlipperFolderForAllApps', this.info.serial);
const installedApps = await this.iOSBridge.getInstalledApps(
this.info.serial,
);
const userApps = installedApps.filter(
({installType}) =>
installType === 'user' || installType === 'user_development',
);
console.debug(
'IOSDevice.readFlipperFolderForAllApps -> found apps',
this.info.serial,
userApps,
);
const appsCommandsResults = await Promise.all(
userApps.map(async (userApp): Promise<DeviceDebugData | undefined> => {
let sonarDirFileNames: string[];
try {
sonarDirFileNames = await this.iOSBridge.ls(
this.info.serial,
userApp.bundleID,
'/Library/Application Support/sonar',
);
} catch (e) {
console.debug(
'IOSDevice.readFlipperFolderForAllApps -> ignoring app as it does not have sonar dir',
this.info.serial,
userApp.bundleID,
);
return;
}
const dir = await tmpDir({unsafeCleanup: true});
const sonarDirContent = await Promise.all(
sonarDirFileNames.map(async (fileName) => {
const filePath = `/Library/Application Support/sonar/${fileName}`;
if (fileName.endsWith('pem')) {
return {
path: filePath,
data: '===SECURE_CONTENT===',
};
}
try {
await this.iOSBridge.pull(
this.info.serial,
filePath,
userApp.bundleID,
dir,
);
} catch (e) {
console.debug(
'IOSDevice.readFlipperFolderForAllApps -> Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:',
this.info.serial,
userApp.bundleID,
fileName,
filePath,
e,
);
await this.iOSBridge.pull(
this.info.serial,
filePath,
userApp.bundleID,
path.join(dir, fileName),
);
console.debug(
'IOSDevice.readFlipperFolderForAllApps -> Subsequent idb pull succeeded. Nevermind previous wranings.',
this.info.serial,
userApp.bundleID,
fileName,
filePath,
);
}
return {
path: filePath,
data: await readFile(path.join(dir, fileName), {
encoding: 'utf-8',
}),
};
}),
);
return {
serial: this.info.serial,
appId: userApp.bundleID,
data: [
{
command: 'iOSBridge.ls /Library/Application Support/sonar',
result: sonarDirFileNames.join('\n'),
},
...sonarDirContent,
],
};
}),
);
return (
appsCommandsResults
// Filter out apps without Flipper integration
.filter((res): res is DeviceDebugData => !!res)
);
}
disconnect() {
if (this.recording) {
this.stopScreenCapture();