Android container utility integration

Summary: Report commands as executed by the android container utility.

Reviewed By: antonk52

Differential Revision: D47340410

fbshipit-source-id: dc2f80572816c8746e603aae2d721da2c47c3c4e
This commit is contained in:
Lorenzo Blasa
2023-07-12 03:30:34 -07:00
committed by Facebook GitHub Bot
parent 6668420083
commit 0e01fcad44
2 changed files with 122 additions and 43 deletions

View File

@@ -17,8 +17,6 @@ import {
import {ClientQuery} from 'flipper-common'; import {ClientQuery} from 'flipper-common';
import {recorder} from '../../recorder'; import {recorder} from '../../recorder';
const logTag = 'AndroidCertificateProvider';
export default class AndroidCertificateProvider extends CertificateProvider { export default class AndroidCertificateProvider extends CertificateProvider {
name = 'AndroidCertificateProvider'; name = 'AndroidCertificateProvider';
medium = 'FS_ACCESS' as const; medium = 'FS_ACCESS' as const;
@@ -34,31 +32,32 @@ export default class AndroidCertificateProvider extends CertificateProvider {
csr: string, csr: string,
): Promise<string> { ): Promise<string> {
recorder.log(clientQuery, 'Query available devices via adb'); recorder.log(clientQuery, 'Query available devices via adb');
const devicesInAdb = await this.adb.listDevices(); const devices = await this.adb.listDevices();
if (devicesInAdb.length === 0) { if (devices.length === 0) {
recorder.error(clientQuery, 'No devices found via adb'); recorder.error(clientQuery, 'No devices found via adb');
throw new Error('No Android devices found'); throw new Error('No Android devices found');
} }
const deviceMatchList = devicesInAdb.map(async (device) => {
const deviceMatches = devices.map(async (device) => {
try { try {
const result = await this.androidDeviceHasMatchingCSR( const result = await this.androidDeviceHasMatchingCSR(
appDirectory, appDirectory,
device.id, device.id,
appName, appName,
csr, csr,
clientQuery,
); );
return {id: device.id, ...result, error: null}; return {id: device.id, ...result, error: null};
} catch (e) { } catch (e) {
console.warn( console.warn(
`[conn] Unable to check for matching CSR in ${device.id}:${appName}`, `[conn] Unable to check for matching CSR in ${device.id}:${appName}`,
logTag,
e, e,
); );
return {id: device.id, isMatch: false, foundCsr: null, error: e}; return {id: device.id, isMatch: false, foundCsr: null, error: e};
} }
}); });
const devices = await Promise.all(deviceMatchList); const matches = await Promise.all(deviceMatches);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); const matchingIds = matches.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) { if (matchingIds.length == 0) {
recorder.error( recorder.error(
@@ -66,11 +65,11 @@ export default class AndroidCertificateProvider extends CertificateProvider {
'Unable to find a matching device for the incoming request', 'Unable to find a matching device for the incoming request',
); );
const erroredDevice = devices.find((d) => d.error); const erroredDevice = matches.find((d) => d.error);
if (erroredDevice) { if (erroredDevice) {
throw erroredDevice.error; throw erroredDevice.error;
} }
const foundCsrs = devices const foundCsrs = matches
.filter((d) => d.foundCsr !== null) .filter((d) => d.foundCsr !== null)
.map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null')); .map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null'));
console.warn( console.warn(
@@ -112,6 +111,7 @@ export default class AndroidCertificateProvider extends CertificateProvider {
appName, appName,
destination + filename, destination + filename,
contents, contents,
clientQuery,
); );
} }
@@ -120,12 +120,14 @@ export default class AndroidCertificateProvider extends CertificateProvider {
deviceId: string, deviceId: string,
processName: string, processName: string,
csr: string, csr: string,
clientQuery: ClientQuery,
): Promise<{isMatch: boolean; foundCsr: string}> { ): Promise<{isMatch: boolean; foundCsr: string}> {
const deviceCsr = await androidUtil.pull( const deviceCsr = await androidUtil.pull(
this.adb, this.adb,
deviceId, deviceId,
processName, processName,
directory + csrFileName, directory + csrFileName,
clientQuery,
); );
// Santitize both of the string before comparation // Santitize both of the string before comparation
// The csr string extraction on client side return string in both way // The csr string extraction on client side return string in both way

View File

@@ -7,8 +7,9 @@
* @format * @format
*/ */
import {UnsupportedError} from 'flipper-common'; import {ClientQuery, UnsupportedError} from 'flipper-common';
import adbkit, {Client} from 'adbkit'; import adbkit, {Client} from 'adbkit';
import {recorder} from '../../recorder';
const allowedAppNameRegex = /^[\w.-]+$/; const allowedAppNameRegex = /^[\w.-]+$/;
const appNotApplicationRegex = /not an application/; const appNotApplicationRegex = /not an application/;
@@ -23,27 +24,29 @@ export type FilePath = string;
export type FileContent = string; export type FileContent = string;
export async function push( export async function push(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: string, app: string,
filepath: string, filepath: string,
contents: string, contents: string,
clientQuery?: ClientQuery,
): Promise<void> { ): Promise<void> {
validateAppName(app); validateAppName(app);
validateFilePath(filepath); validateFilePath(filepath);
validateFileContent(contents); validateFileContent(contents);
return await _push(client, deviceId, app, filepath, contents); return await _push(adbClient, deviceId, app, filepath, contents, clientQuery);
} }
export async function pull( export async function pull(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: string, app: string,
path: string, path: string,
clientQuery?: ClientQuery,
): Promise<string> { ): Promise<string> {
validateAppName(app); validateAppName(app);
validateFilePath(path); validateFilePath(path);
return await _pull(client, deviceId, app, path); return await _pull(adbClient, deviceId, app, path, clientQuery);
} }
function validateAppName(app: string): void { function validateAppName(app: string): void {
@@ -80,54 +83,129 @@ class RunAsError extends Error {
} }
} }
function _push( async function _push(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: AppName, app: AppName,
filename: FilePath, filename: FilePath,
contents: FileContent, contents: FileContent,
clientQuery?: ClientQuery,
): Promise<void> { ): Promise<void> {
console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag); console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag);
// TODO: this is sensitive to escaping issues, can we leverage client.push instead?
// https://www.npmjs.com/package/adbkit#pushing-a-file-to-all-connected-devices const cmd = `echo "${contents}" > '${filename}' && chmod 644 '${filename}'`;
const command = `echo "${contents}" > '${filename}' && chmod 644 '${filename}'`; const description = 'Push file to device using adb shell (echo / chmod)';
return executeCommandAsApp(client, deviceId, app, command) const troubleshoot = 'adb may be unresponsive, try `adb kill-server`';
.then((_) => undefined)
.catch((error) => { const reportSuccess = () => {
if (error instanceof RunAsError) { recorder.event('cmd', {
// Fall back to running the command directly. This will work if adb is running as root. cmd,
executeCommandWithSu(client, deviceId, app, command, error); description,
return undefined; troubleshoot,
} success: true,
throw error; context: clientQuery,
}); });
};
const reportFailure = (error: Error) => {
recorder.event('cmd', {
cmd,
description,
troubleshoot,
stdout: error.message,
success: false,
context: clientQuery,
});
};
try {
await executeCommandAsApp(adbClient, deviceId, app, cmd);
reportSuccess();
} catch (error) {
if (error instanceof RunAsError) {
// Fall back to running the command directly.
// This will work if adb is running as root.
try {
await executeCommandWithSu(adbClient, deviceId, app, cmd, error);
reportSuccess();
return;
} catch (suError) {
reportFailure(suError);
throw suError;
}
}
reportFailure(error);
throw error;
}
} }
function _pull( async function _pull(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: AppName, app: AppName,
path: FilePath, path: FilePath,
clientQuery?: ClientQuery,
): Promise<string> { ): Promise<string> {
const command = `cat '${path}'`; const cmd = `cat '${path}'`;
return executeCommandAsApp(client, deviceId, app, command).catch((error) => { const description = 'Pull file from device using adb shell (cat)';
const troubleshoot = 'adb may be unresponsive, try `adb kill-server`';
const reportSuccess = () => {
recorder.event('cmd', {
cmd,
description,
troubleshoot,
success: true,
context: clientQuery,
});
};
const reportFailure = (error: Error) => {
recorder.event('cmd', {
cmd,
description,
troubleshoot,
stdout: error.message,
success: false,
context: clientQuery,
});
};
try {
const content = await executeCommandAsApp(adbClient, deviceId, app, cmd);
reportSuccess();
return content;
} catch (error) {
if (error instanceof RunAsError) { if (error instanceof RunAsError) {
// Fall back to running the command directly. This will work if adb is running as root. // Fall back to running the command directly.
return executeCommandWithSu(client, deviceId, app, command, error); // This will work if adb is running as root.
try {
const content = await executeCommandWithSu(
adbClient,
deviceId,
app,
cmd,
error,
);
reportSuccess();
return content;
} catch (suError) {
reportFailure(suError);
throw suError;
}
} }
reportFailure(error);
throw error; throw error;
}); }
} }
// Keep this method private since it relies on pre-validated arguments // Keep this method private since it relies on pre-validated arguments
export function executeCommandAsApp( export function executeCommandAsApp(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: string, app: string,
command: string, command: string,
): Promise<string> { ): Promise<string> {
return _executeCommandWithRunner( return _executeCommandWithRunner(
client, adbClient,
deviceId, deviceId,
app, app,
command, command,
@@ -136,28 +214,27 @@ export function executeCommandAsApp(
} }
async function executeCommandWithSu( async function executeCommandWithSu(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: string, app: string,
command: string, command: string,
originalErrorToThrow: RunAsError, originalErrorToThrow: RunAsError,
): Promise<string> { ): Promise<string> {
try { try {
return _executeCommandWithRunner(client, deviceId, app, command, 'su'); return _executeCommandWithRunner(adbClient, deviceId, app, command, 'su');
} catch (e) { } catch (e) {
console.debug(e);
throw originalErrorToThrow; throw originalErrorToThrow;
} }
} }
function _executeCommandWithRunner( function _executeCommandWithRunner(
client: Client, adbClient: Client,
deviceId: string, deviceId: string,
app: string, app: string,
command: string, command: string,
runner: string, runner: string,
): Promise<string> { ): Promise<string> {
return client return adbClient
.shell(deviceId, `echo '${command}' | ${runner}`) .shell(deviceId, `echo '${command}' | ${runner}`)
.then(adbkit.util.readAll) .then(adbkit.util.readAll)
.then((buffer) => buffer.toString()) .then((buffer) => buffer.toString())