Centralise logging
Summary: Centralise connectivity logging into a single place. By having all logs go through a single interface, then it becomes trivial to manipulate them as needed. In this change, this is not done. In subsequent diffs, logs will be dispatched via an event and will be visualised in the Connectivity Hub. Reviewed By: passy Differential Revision: D47185054 fbshipit-source-id: fb5eab98895be0c8f61fb9a77d3e66d6a8dbcb27
This commit is contained in:
committed by
Facebook GitHub Bot
parent
49d1a8b0fa
commit
fc38355eee
@@ -12,7 +12,12 @@ import {FlipperServerImpl} from '../FlipperServerImpl';
|
||||
import {ServerDevice} from './ServerDevice';
|
||||
|
||||
/**
|
||||
* Use this device when you do not have the actual uuid of the device. For example, it is currently used in the case when, we do certificate exchange through WWW mode. In this mode we do not know the device id of the app and we generate a fake one.
|
||||
* Use this device when you do not have the actual uuid of the device.
|
||||
* For example, it is currently used in the case when, we do certificate
|
||||
* exchange through WWW mode.
|
||||
*
|
||||
* In this mode we do not know the device id of the app and we
|
||||
* generate a fake one.
|
||||
*/
|
||||
export default class DummyDevice extends ServerDevice {
|
||||
constructor(
|
||||
|
||||
@@ -58,30 +58,28 @@ export abstract class ServerDevice {
|
||||
}
|
||||
|
||||
async startScreenCapture(_destination: string): Promise<void> {
|
||||
throw new Error('startScreenCapture not implemented on BaseDevice ');
|
||||
throw new Error('startScreenCapture not implemented');
|
||||
}
|
||||
|
||||
async stopScreenCapture(): Promise<string> {
|
||||
throw new Error('stopScreenCapture not implemented on BaseDevice ');
|
||||
throw new Error('stopScreenCapture not implemented');
|
||||
}
|
||||
|
||||
async executeShell(_command: string): Promise<string> {
|
||||
throw new Error('executeShell not implemented on BaseDevice');
|
||||
throw new Error('executeShell not implemented');
|
||||
}
|
||||
|
||||
async forwardPort(_local: string, _remote: string): Promise<boolean> {
|
||||
throw new Error('forwardPort not implemented on BaseDevice');
|
||||
throw new Error('forwardPort not implemented');
|
||||
}
|
||||
|
||||
async clearLogs(): Promise<void> {
|
||||
// no-op on most devices
|
||||
}
|
||||
async clearLogs(): Promise<void> {}
|
||||
|
||||
async navigateToLocation(_location: string) {
|
||||
throw new Error('navigateLocation not implemented on BaseDevice');
|
||||
throw new Error('navigateLocation not implemented');
|
||||
}
|
||||
|
||||
async installApp(_appBundlePath: string): Promise<void> {
|
||||
throw new Error('Install not implemented');
|
||||
throw new Error('installApp not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Client} from 'adbkit';
|
||||
import * as androidUtil from './androidContainerUtility';
|
||||
import {
|
||||
csrFileName,
|
||||
extractAppNameFromCSR,
|
||||
extractBundleIdFromCSR,
|
||||
} from '../../app-connectivity/certificate-exchange/certificate-utils';
|
||||
|
||||
const logTag = 'AndroidCertificateProvider';
|
||||
@@ -98,7 +98,7 @@ export default class AndroidCertificateProvider extends CertificateProvider {
|
||||
contents: string,
|
||||
csr: string,
|
||||
) {
|
||||
const appName = await extractAppNameFromCSR(csr);
|
||||
const appName = await extractBundleIdFromCSR(csr);
|
||||
const deviceId = await this.getTargetDeviceId(appName, destination, csr);
|
||||
await androidUtil.push(
|
||||
this.adb,
|
||||
|
||||
@@ -197,16 +197,14 @@ export class SimctlBridge implements IOSBridge {
|
||||
async getInstalledApps(
|
||||
_serial: string,
|
||||
): Promise<IOSInstalledAppDescriptor[]> {
|
||||
// TODO: Implement me
|
||||
throw new Error(
|
||||
'SimctlBridge does not support getInstalledApps. Install IDB (https://fbidb.io/).',
|
||||
'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/).',
|
||||
'SimctlBridge does not support ls. Install idb (https://fbidb.io/).',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,7 +352,7 @@ function makeTempScreenshotFilePath() {
|
||||
}
|
||||
|
||||
async function unzip(filePath: string, destination: string): Promise<void> {
|
||||
//todo this probably shouldn't involve shelling out...
|
||||
// TODO: 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(
|
||||
@@ -381,12 +379,11 @@ export async function makeIOSBridge(
|
||||
enablePhysicalDevices: boolean,
|
||||
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
|
||||
): Promise<IOSBridge> {
|
||||
// prefer idb
|
||||
if (await isAvailableFn(idbPath)) {
|
||||
return new IDBBridge(idbPath, enablePhysicalDevices);
|
||||
}
|
||||
|
||||
// no idb, if it's a simulator and xcode is available, we can use xcrun
|
||||
// If Xcode is available then xcrun instead of idb is used.
|
||||
if (isXcodeDetected) {
|
||||
return new SimctlBridge();
|
||||
}
|
||||
|
||||
@@ -58,14 +58,16 @@ export default class IOSDevice
|
||||
this.serial,
|
||||
this.info.deviceType,
|
||||
);
|
||||
// It is OK not to await the start of the log listener. We just spawn it and handle errors internally.
|
||||
// It is OK not to await the start of the log listener.
|
||||
// We just spawn it and handle errors internally.
|
||||
this.logListener
|
||||
.start()
|
||||
.catch((e) =>
|
||||
console.error('IOSDevice.logListener.start -> unexpected error', e),
|
||||
);
|
||||
this.crashWatcher = new iOSCrashWatcher(this);
|
||||
// It is OK not to await the start of the crash watcher. We just spawn it and handle errors internally.
|
||||
// It is OK not to await the start of the crash watcher.
|
||||
// We just spawn it and handle errors internally.
|
||||
this.crashWatcher
|
||||
.start()
|
||||
.catch((e) =>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {promisify} from 'util';
|
||||
import tmp, {DirOptions} from 'tmp';
|
||||
import {
|
||||
csrFileName,
|
||||
extractAppNameFromCSR,
|
||||
extractBundleIdFromCSR,
|
||||
} from '../../app-connectivity/certificate-exchange/certificate-utils';
|
||||
import path from 'path';
|
||||
|
||||
@@ -37,9 +37,11 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
): Promise<string> {
|
||||
const matches = /\/Devices\/([^/]+)\//.exec(appDirectory);
|
||||
if (matches && matches.length == 2) {
|
||||
// It's a simulator, the deviceId is in the filepath.
|
||||
// It's a simulator, the device identifier is in the filepath.
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
// Get all available targets
|
||||
const targets = await iosUtil.targets(
|
||||
this.idbConfig.idbPath,
|
||||
this.idbConfig.enablePhysicalIOS,
|
||||
@@ -58,7 +60,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
return {id: target.udid, isMatch};
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Unable to check for matching CSR in ${target.udid}:${appName}`,
|
||||
`[conn] Unable to check for matching CSR in ${target.udid}:${appName}`,
|
||||
logTag,
|
||||
e,
|
||||
);
|
||||
@@ -71,7 +73,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
throw new Error(`No matching device found for app: ${appName}`);
|
||||
}
|
||||
if (matchingIds.length > 1) {
|
||||
console.warn(`Multiple devices found for app: ${appName}`);
|
||||
console.warn(`[conn] Multiple devices found for app: ${appName}`);
|
||||
}
|
||||
return matchingIds[0];
|
||||
}
|
||||
@@ -82,37 +84,33 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
contents: string,
|
||||
csr: string,
|
||||
) {
|
||||
console.debug('iOSCertificateProvider.deployOrStageFileForDevice', {
|
||||
console.debug('[conn] Deploying file to device ', {
|
||||
destination,
|
||||
filename,
|
||||
});
|
||||
const appName = await extractAppNameFromCSR(csr);
|
||||
const bundleId = await extractBundleIdFromCSR(csr);
|
||||
try {
|
||||
await fs.writeFile(destination + filename, contents);
|
||||
} catch (err) {
|
||||
// Writing directly to FS failed. It's probably a physical device.
|
||||
console.debug(
|
||||
'iOSCertificateProvider.deployOrStageFileForDevice -> physical device',
|
||||
'[conn] Deploying file using idb as physical device is inferred',
|
||||
);
|
||||
const relativePathInsideApp =
|
||||
this.getRelativePathInAppContainer(destination);
|
||||
|
||||
console.debug(
|
||||
'iOSCertificateProvider.deployOrStageFileForDevice: realtive path',
|
||||
relativePathInsideApp,
|
||||
);
|
||||
console.debug(`[conn] Relative path '${relativePathInsideApp}'`);
|
||||
|
||||
const udid = await this.getTargetDeviceId(appName, destination, csr);
|
||||
const udid = await this.getTargetDeviceId(bundleId, destination, csr);
|
||||
await this.pushFileToiOSDevice(
|
||||
udid,
|
||||
appName,
|
||||
bundleId,
|
||||
relativePathInsideApp,
|
||||
filename,
|
||||
contents,
|
||||
);
|
||||
}
|
||||
|
||||
console.debug('iOSCertificateProvider.deployOrStageFileForDevice -> done');
|
||||
console.debug('[conn] Deploying file to device complete');
|
||||
}
|
||||
|
||||
private getRelativePathInAppContainer(absolutePath: string) {
|
||||
@@ -131,12 +129,12 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
contents: string,
|
||||
): Promise<void> {
|
||||
const dir = await tmpDir({unsafeCleanup: true});
|
||||
const filePath = path.resolve(dir, filename);
|
||||
await fs.writeFile(filePath, contents);
|
||||
const src = path.resolve(dir, filename);
|
||||
await fs.writeFile(src, contents);
|
||||
|
||||
await iosUtil.push(
|
||||
udid,
|
||||
filePath,
|
||||
src,
|
||||
bundleId,
|
||||
destination,
|
||||
this.idbConfig.idbPath,
|
||||
@@ -149,69 +147,46 @@ export default class iOSCertificateProvider extends CertificateProvider {
|
||||
bundleId: string,
|
||||
csr: string,
|
||||
): Promise<boolean> {
|
||||
const originalFile = this.getRelativePathInAppContainer(
|
||||
const src = this.getRelativePathInAppContainer(
|
||||
path.resolve(directory, csrFileName),
|
||||
);
|
||||
const dir = await tmpDir({unsafeCleanup: true});
|
||||
const dst = await tmpDir({unsafeCleanup: true});
|
||||
|
||||
// Workaround for idb weirdness
|
||||
// Originally started at D27590885
|
||||
// Re-appared at https://github.com/facebook/flipper/issues/3009
|
||||
//
|
||||
// People reported various workarounds. None of them worked consistently for everyone.
|
||||
// Usually, the workarounds included re-building idb from source or re-installing it.
|
||||
//
|
||||
// The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not.
|
||||
//
|
||||
// This is how idb_companion sets its locality
|
||||
// https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507
|
||||
// idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file.
|
||||
//
|
||||
// So when it is "local", the pulled filed is written directly to the destination path
|
||||
// https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698
|
||||
// So it is expected that the destination path ends with a file to write to.
|
||||
// However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232
|
||||
// Where we create a tree of directories and write the file stream there.
|
||||
//
|
||||
// So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion.
|
||||
//
|
||||
// The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it.
|
||||
try {
|
||||
await iosUtil.pull(
|
||||
deviceId,
|
||||
originalFile,
|
||||
bundleId,
|
||||
dir,
|
||||
this.idbConfig.idbPath,
|
||||
);
|
||||
await iosUtil.pull(deviceId, src, bundleId, dst, this.idbConfig.idbPath);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'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:',
|
||||
`[conn] 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.`,
|
||||
e,
|
||||
);
|
||||
await iosUtil.pull(
|
||||
deviceId,
|
||||
originalFile,
|
||||
src,
|
||||
bundleId,
|
||||
path.join(dir, csrFileName),
|
||||
path.join(dst, csrFileName),
|
||||
this.idbConfig.idbPath,
|
||||
);
|
||||
console.info(
|
||||
'Subsequent idb pull succeeded. Nevermind previous wranings.',
|
||||
'[conn] Subsequent idb pull succeeded. Nevermind previous wranings.',
|
||||
);
|
||||
}
|
||||
|
||||
const items = await fs.readdir(dir);
|
||||
const items = await fs.readdir(dst);
|
||||
if (items.length > 1) {
|
||||
throw new Error('Conflict in temp dir');
|
||||
throw new Error('Conflict in temporary dir');
|
||||
}
|
||||
if (items.length === 0) {
|
||||
throw new Error('Failed to pull CSR from device');
|
||||
throw new Error('No CSR found on device');
|
||||
}
|
||||
const fileName = items[0];
|
||||
const copiedFile = path.resolve(dir, fileName);
|
||||
console.debug('Trying to read CSR from', copiedFile);
|
||||
const data = await fs.readFile(copiedFile);
|
||||
|
||||
const filename = items[0];
|
||||
const pulledFile = path.resolve(dst, filename);
|
||||
|
||||
console.debug(`[conn] Read CSR from '${pulledFile}'`);
|
||||
|
||||
const data = await fs.readFile(pulledFile);
|
||||
const csrFromDevice = this.santitizeString(data.toString());
|
||||
return csrFromDevice === this.santitizeString(csr);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {promisify} from 'util';
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {recorder} from '../../recorder';
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
export type IdbConfig = {
|
||||
@@ -69,12 +70,22 @@ async function safeExec(
|
||||
|
||||
async function queryTargetsWithXcode(): Promise<Array<DeviceTarget>> {
|
||||
const cmd = 'xcrun xctrace list devices';
|
||||
const description = 'Query available devices with Xcode';
|
||||
const troubleshoot = `Xcode command line tools are not installed.
|
||||
Run 'xcode-select --install' from terminal.`;
|
||||
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
if (!stdout) {
|
||||
recorder.event('cmd', {cmd, description, success: false, troubleshoot});
|
||||
throw new Error('No output from command');
|
||||
}
|
||||
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
description,
|
||||
success: true,
|
||||
stdout: stdout.toString(),
|
||||
});
|
||||
return stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
@@ -87,7 +98,13 @@ async function queryTargetsWithXcode(): Promise<Array<DeviceTarget>> {
|
||||
return {udid, type: 'physical', name};
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Failed to query devices using '${cmd}'`, e);
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
description,
|
||||
success: false,
|
||||
troubleshoot,
|
||||
stderr: e.toString(),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -96,14 +113,33 @@ async function queryTargetsWithIdb(
|
||||
idbPath: string,
|
||||
): Promise<Array<DeviceTarget>> {
|
||||
const cmd = `${idbPath} list-targets --json`;
|
||||
const description = 'Query available devices with idb';
|
||||
const troubleshoot = `Either idb is not installed or needs to be reset.
|
||||
Run 'idb kill' from terminal.`;
|
||||
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
if (!stdout) {
|
||||
recorder.event('cmd', {cmd, description, success: false, troubleshoot});
|
||||
throw new Error('No output from command');
|
||||
}
|
||||
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
description,
|
||||
success: true,
|
||||
stdout: stdout.toString(),
|
||||
});
|
||||
|
||||
return parseIdbTargets(stdout.toString());
|
||||
} catch (e) {
|
||||
console.warn(`Failed to execute '${cmd}' for targets.`, e);
|
||||
recorder.event('cmd', {
|
||||
cmd,
|
||||
description,
|
||||
success: false,
|
||||
troubleshoot,
|
||||
stderr: e.toString(),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -114,6 +150,7 @@ async function queryTargetsWithIdbCompanion(
|
||||
): Promise<Array<DeviceTarget>> {
|
||||
if (await isAvailable(idbCompanionPath)) {
|
||||
const cmd = `${idbCompanionPath} --list 1 --only device`;
|
||||
recorder.rawLog(`Query devices with idb companion '${cmd}'`);
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
if (!stdout) {
|
||||
@@ -122,18 +159,18 @@ async function queryTargetsWithIdbCompanion(
|
||||
|
||||
const devices = parseIdbTargets(stdout.toString());
|
||||
if (devices.length > 0 && !isPhysicalDeviceEnabled) {
|
||||
console.warn(
|
||||
recorder.rawError(
|
||||
`You are trying to connect Physical Device.
|
||||
Please enable the toggle "Enable physical iOS device" from the setting screen.`,
|
||||
);
|
||||
}
|
||||
return devices;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to execute '${cmd}' for targets:`, e);
|
||||
recorder.rawError(`Failed to query devices using '${cmd}'`, e);
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
recorder.rawError(
|
||||
`Unable to locate idb_companion in '${idbCompanionPath}'.
|
||||
Try running sudo yum install -y fb-idb`,
|
||||
);
|
||||
@@ -177,6 +214,7 @@ async function idbDescribeTarget(
|
||||
idbPath: string,
|
||||
): Promise<DeviceTarget | undefined> {
|
||||
const cmd = `${idbPath} describe --json`;
|
||||
recorder.rawLog(`Describe target '${cmd}'`);
|
||||
try {
|
||||
const {stdout} = await safeExec(cmd);
|
||||
if (!stdout) {
|
||||
@@ -184,7 +222,7 @@ async function idbDescribeTarget(
|
||||
}
|
||||
return parseIdbTarget(stdout.toString());
|
||||
} catch (e) {
|
||||
console.warn(`Failed to execute '${cmd}' to describe a target.`, e);
|
||||
recorder.rawError(`Failed to execute '${cmd}' to describe a target.`, e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -209,8 +247,7 @@ async function targets(
|
||||
const isXcodeInstalled = await isXcodeDetected();
|
||||
if (!isXcodeInstalled) {
|
||||
if (!isPhysicalDeviceEnabled) {
|
||||
// TODO: Show a notification to enable the toggle or integrate Doctor to better suggest this advice.
|
||||
console.warn(
|
||||
recorder.rawError(
|
||||
'You are trying to connect a physical device. Please enable the toggle "Enable physical iOS device" from the setting screen.',
|
||||
);
|
||||
}
|
||||
@@ -243,11 +280,13 @@ async function push(
|
||||
await memoize(checkIdbIsInstalled)(idbPath);
|
||||
|
||||
const push_ = async () => {
|
||||
const cmd = `${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`;
|
||||
try {
|
||||
await safeExec(
|
||||
`${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`,
|
||||
);
|
||||
recorder.rawLog(`Push file to device '${cmd}'`);
|
||||
await safeExec(cmd);
|
||||
recorder.rawLog(`Successfully pushed file to device`);
|
||||
} catch (e) {
|
||||
recorder.rawError(`Failed to push file to device`, e);
|
||||
handleMissingIdb(e, idbPath);
|
||||
throw e;
|
||||
}
|
||||
@@ -266,11 +305,13 @@ async function pull(
|
||||
await memoize(checkIdbIsInstalled)(idbPath);
|
||||
|
||||
const pull_ = async () => {
|
||||
const cmd = `${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`;
|
||||
try {
|
||||
await safeExec(
|
||||
`${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`,
|
||||
);
|
||||
recorder.rawLog(`Pull file from device '${cmd}'`);
|
||||
await safeExec(cmd);
|
||||
recorder.rawLog(`Successfully pulled file from device`);
|
||||
} catch (e) {
|
||||
recorder.rawError(`Failed to pull file from device`, e);
|
||||
handleMissingIdb(e, idbPath);
|
||||
handleMissingPermissions(e);
|
||||
throw e;
|
||||
|
||||
@@ -72,7 +72,8 @@ export function shouldShowiOSCrashNotification(
|
||||
): boolean {
|
||||
const appPath = legacy ? parsePathLegacy(content) : parsePathModern(content);
|
||||
if (!appPath || !appPath.includes(serial)) {
|
||||
// Do not show notifications for the app which are not running on this device
|
||||
// Do not show notifications for the app which
|
||||
// are not running on this device.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -117,7 +117,7 @@ export class IOSDeviceManager {
|
||||
if (currentDeviceIDs.has(udid)) {
|
||||
currentDeviceIDs.delete(udid);
|
||||
} else {
|
||||
console.info(`[conn] detected new iOS device ${udid}`, activeDevice);
|
||||
console.info(`[conn] Detected new iOS device ${udid}`, activeDevice);
|
||||
const iOSDevice = new IOSDevice(
|
||||
this.flipperServer,
|
||||
bridge,
|
||||
|
||||
Reference in New Issue
Block a user