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
@@ -605,7 +605,11 @@ export class FlipperServerImpl implements FlipperServer {
|
|||||||
private async fetchDebugLogs() {
|
private async fetchDebugLogs() {
|
||||||
const debugDataForEachDevice = await Promise.all(
|
const debugDataForEachDevice = await Promise.all(
|
||||||
[...this.devices.values()]
|
[...this.devices.values()]
|
||||||
.filter((device) => device.info.os === 'Android')
|
.filter(
|
||||||
|
(device) =>
|
||||||
|
device.connected &&
|
||||||
|
(device.info.os === 'Android' || device.info.os === 'iOS'),
|
||||||
|
)
|
||||||
.map((device) =>
|
.map((device) =>
|
||||||
(device as unknown as DebuggableDevice).readFlipperFolderForAllApps(),
|
(device as unknown as DebuggableDevice).readFlipperFolderForAllApps(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import child_process from 'child_process';
|
|||||||
import type {IOSDeviceParams} from 'flipper-common';
|
import type {IOSDeviceParams} from 'flipper-common';
|
||||||
import {DeviceType, uuid} from 'flipper-common';
|
import {DeviceType, uuid} from 'flipper-common';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {exec, execFile} from 'promisify-child-process';
|
import {ChildProcessPromise, exec, execFile} from 'promisify-child-process';
|
||||||
import {getFlipperServerConfig} from '../../FlipperServerConfig';
|
import {getFlipperServerConfig} from '../../FlipperServerConfig';
|
||||||
|
import iOSContainerUtility from './iOSContainerUtility';
|
||||||
export const ERR_NO_IDB_OR_XCODE_AVAILABLE =
|
export const ERR_NO_IDB_OR_XCODE_AVAILABLE =
|
||||||
'Neither Xcode nor idb available. Cannot provide iOS device functionality.';
|
'Neither Xcode nor idb available. Cannot provide iOS device functionality.';
|
||||||
|
|
||||||
@@ -31,6 +32,16 @@ type iOSSimulatorDevice = {
|
|||||||
udid: string;
|
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 {
|
export interface IOSBridge {
|
||||||
startLogListener: (
|
startLogListener: (
|
||||||
udid: string,
|
udid: string,
|
||||||
@@ -48,6 +59,14 @@ export interface IOSBridge {
|
|||||||
ipaPath: string,
|
ipaPath: string,
|
||||||
tempPath: string,
|
tempPath: string,
|
||||||
) => Promise<void>;
|
) => 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 {
|
export class IDBBridge implements IOSBridge {
|
||||||
@@ -56,6 +75,66 @@ export class IDBBridge implements IOSBridge {
|
|||||||
private enablePhysicalDevices: boolean,
|
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> {
|
async installApp(serial: string, ipaPath: string): Promise<void> {
|
||||||
console.log(`Installing app via IDB ${ipaPath} ${serial}`);
|
console.log(`Installing app via IDB ${ipaPath} ${serial}`);
|
||||||
await this._execIdb(`install ${ipaPath} --udid ${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}`);
|
return exec(`${this.idbPath} ${command}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SimctlBridge implements IOSBridge {
|
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(
|
async installApp(
|
||||||
serial: string,
|
serial: string,
|
||||||
ipaPath: string,
|
ipaPath: string,
|
||||||
|
|||||||
@@ -7,15 +7,25 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DeviceType, timeout} from 'flipper-common';
|
import {DeviceDebugData, DeviceType, timeout} from 'flipper-common';
|
||||||
import {ChildProcess} from 'child_process';
|
import {ChildProcess} from 'child_process';
|
||||||
import {IOSBridge} from './IOSBridge';
|
import {IOSBridge} from './IOSBridge';
|
||||||
import {ServerDevice} from '../ServerDevice';
|
import {ServerDevice} from '../ServerDevice';
|
||||||
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
import {FlipperServerImpl} from '../../FlipperServerImpl';
|
||||||
import {iOSCrashWatcher} from './iOSCrashUtils';
|
import {iOSCrashWatcher} from './iOSCrashUtils';
|
||||||
import {iOSLogListener} from './iOSLogListener';
|
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 recording?: {process: ChildProcess; destination: string};
|
||||||
private iOSBridge: IOSBridge;
|
private iOSBridge: IOSBridge;
|
||||||
readonly logListener: iOSLogListener;
|
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() {
|
disconnect() {
|
||||||
if (this.recording) {
|
if (this.recording) {
|
||||||
this.stopScreenCapture();
|
this.stopScreenCapture();
|
||||||
|
|||||||
Reference in New Issue
Block a user