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:
committed by
Facebook GitHub Bot
parent
3c8ebf105a
commit
821bf2b5b7
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user