Skip to content

Commit

Permalink
Android container utility integration
Browse files Browse the repository at this point in the history
Summary: Report commands as executed by the android container utility.

Reviewed By: antonk52

Differential Revision: D47340410

fbshipit-source-id: dc2f80572816c8746e603aae2d721da2c47c3c4e
  • Loading branch information
lblasa authored and facebook-github-bot committed Jul 12, 2023
1 parent 6668420 commit 0e01fca
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import {
import {ClientQuery} from 'flipper-common';
import {recorder} from '../../recorder';

const logTag = 'AndroidCertificateProvider';

export default class AndroidCertificateProvider extends CertificateProvider {
name = 'AndroidCertificateProvider';
medium = 'FS_ACCESS' as const;
Expand All @@ -34,43 +32,44 @@ export default class AndroidCertificateProvider extends CertificateProvider {
csr: string,
): Promise<string> {
recorder.log(clientQuery, 'Query available devices via adb');
const devicesInAdb = await this.adb.listDevices();
if (devicesInAdb.length === 0) {
const devices = await this.adb.listDevices();
if (devices.length === 0) {
recorder.error(clientQuery, 'No devices found via adb');
throw new Error('No Android devices found');
}
const deviceMatchList = devicesInAdb.map(async (device) => {

const deviceMatches = devices.map(async (device) => {
try {
const result = await this.androidDeviceHasMatchingCSR(
appDirectory,
device.id,
appName,
csr,
clientQuery,
);
return {id: device.id, ...result, error: null};
} catch (e) {
console.warn(
`[conn] Unable to check for matching CSR in ${device.id}:${appName}`,
logTag,
e,
);
return {id: device.id, isMatch: false, foundCsr: null, error: e};
}
});
const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
const matches = await Promise.all(deviceMatches);
const matchingIds = matches.filter((m) => m.isMatch).map((m) => m.id);

if (matchingIds.length == 0) {
recorder.error(
clientQuery,
'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) {
throw erroredDevice.error;
}
const foundCsrs = devices
const foundCsrs = matches
.filter((d) => d.foundCsr !== null)
.map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null'));
console.warn(
Expand Down Expand Up @@ -112,6 +111,7 @@ export default class AndroidCertificateProvider extends CertificateProvider {
appName,
destination + filename,
contents,
clientQuery,
);
}

Expand All @@ -120,12 +120,14 @@ export default class AndroidCertificateProvider extends CertificateProvider {
deviceId: string,
processName: string,
csr: string,
clientQuery: ClientQuery,
): Promise<{isMatch: boolean; foundCsr: string}> {
const deviceCsr = await androidUtil.pull(
this.adb,
deviceId,
processName,
directory + csrFileName,
clientQuery,
);
// Santitize both of the string before comparation
// The csr string extraction on client side return string in both way
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* @format
*/

import {UnsupportedError} from 'flipper-common';
import {ClientQuery, UnsupportedError} from 'flipper-common';
import adbkit, {Client} from 'adbkit';
import {recorder} from '../../recorder';

const allowedAppNameRegex = /^[\w.-]+$/;
const appNotApplicationRegex = /not an application/;
Expand All @@ -23,27 +24,29 @@ export type FilePath = string;
export type FileContent = string;

export async function push(
client: Client,
adbClient: Client,
deviceId: string,
app: string,
filepath: string,
contents: string,
clientQuery?: ClientQuery,
): Promise<void> {
validateAppName(app);
validateFilePath(filepath);
validateFileContent(contents);
return await _push(client, deviceId, app, filepath, contents);
return await _push(adbClient, deviceId, app, filepath, contents, clientQuery);
}

export async function pull(
client: Client,
adbClient: Client,
deviceId: string,
app: string,
path: string,
clientQuery?: ClientQuery,
): Promise<string> {
validateAppName(app);
validateFilePath(path);
return await _pull(client, deviceId, app, path);
return await _pull(adbClient, deviceId, app, path, clientQuery);
}

function validateAppName(app: string): void {
Expand Down Expand Up @@ -80,54 +83,129 @@ class RunAsError extends Error {
}
}

function _push(
client: Client,
async function _push(
adbClient: Client,
deviceId: string,
app: AppName,
filename: FilePath,
contents: FileContent,
clientQuery?: ClientQuery,
): Promise<void> {
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 command = `echo "${contents}" > '${filename}' && chmod 644 '${filename}'`;
return executeCommandAsApp(client, deviceId, app, command)
.then((_) => undefined)
.catch((error) => {
if (error instanceof RunAsError) {
// Fall back to running the command directly. This will work if adb is running as root.
executeCommandWithSu(client, deviceId, app, command, error);
return undefined;
}
throw error;

const cmd = `echo "${contents}" > '${filename}' && chmod 644 '${filename}'`;
const description = 'Push file to device using adb shell (echo / chmod)';
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 {
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(
client: Client,
async function _pull(
adbClient: Client,
deviceId: string,
app: AppName,
path: FilePath,
clientQuery?: ClientQuery,
): Promise<string> {
const command = `cat '${path}'`;
return executeCommandAsApp(client, deviceId, app, command).catch((error) => {
const cmd = `cat '${path}'`;
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) {
// Fall back to running the command directly. This will work if adb is running as root.
return executeCommandWithSu(client, deviceId, app, command, error);
// Fall back to running the command directly.
// 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;
});
}
}

// Keep this method private since it relies on pre-validated arguments
export function executeCommandAsApp(
client: Client,
adbClient: Client,
deviceId: string,
app: string,
command: string,
): Promise<string> {
return _executeCommandWithRunner(
client,
adbClient,
deviceId,
app,
command,
Expand All @@ -136,28 +214,27 @@ export function executeCommandAsApp(
}

async function executeCommandWithSu(
client: Client,
adbClient: Client,
deviceId: string,
app: string,
command: string,
originalErrorToThrow: RunAsError,
): Promise<string> {
try {
return _executeCommandWithRunner(client, deviceId, app, command, 'su');
return _executeCommandWithRunner(adbClient, deviceId, app, command, 'su');
} catch (e) {
console.debug(e);
throw originalErrorToThrow;
}
}

function _executeCommandWithRunner(
client: Client,
adbClient: Client,
deviceId: string,
app: string,
command: string,
runner: string,
): Promise<string> {
return client
return adbClient
.shell(deviceId, `echo '${command}' | ${runner}`)
.then(adbkit.util.readAll)
.then((buffer) => buffer.toString())
Expand Down

0 comments on commit 0e01fca

Please sign in to comment.