From 0e01fcad443d66f0dda8e8e16d5d209f257b1c36 Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Wed, 12 Jul 2023 03:30:34 -0700 Subject: [PATCH] Android container utility integration Summary: Report commands as executed by the android container utility. Reviewed By: antonk52 Differential Revision: D47340410 fbshipit-source-id: dc2f80572816c8746e603aae2d721da2c47c3c4e --- .../android/AndroidCertificateProvider.tsx | 22 +-- .../android/androidContainerUtility.tsx | 143 ++++++++++++++---- 2 files changed, 122 insertions(+), 43 deletions(-) diff --git a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx index 1543c719c..6b8c293b2 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx @@ -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; @@ -34,31 +32,32 @@ export default class AndroidCertificateProvider extends CertificateProvider { csr: string, ): Promise { 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( @@ -66,11 +65,11 @@ export default class AndroidCertificateProvider extends CertificateProvider { '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( @@ -112,6 +111,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { appName, destination + filename, contents, + clientQuery, ); } @@ -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 diff --git a/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx b/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx index ae5464c10..3ddac60d9 100644 --- a/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx +++ b/desktop/flipper-server-core/src/devices/android/androidContainerUtility.tsx @@ -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/; @@ -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 { 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 { validateAppName(app); validateFilePath(path); - return await _pull(client, deviceId, app, path); + return await _pull(adbClient, deviceId, app, path, clientQuery); } function validateAppName(app: string): void { @@ -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 { 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 { - 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 { return _executeCommandWithRunner( - client, + adbClient, deviceId, app, command, @@ -136,28 +214,27 @@ export function executeCommandAsApp( } async function executeCommandWithSu( - client: Client, + adbClient: Client, deviceId: string, app: string, command: string, originalErrorToThrow: RunAsError, ): Promise { 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 { - return client + return adbClient .shell(deviceId, `echo '${command}' | ${runner}`) .then(adbkit.util.readAll) .then((buffer) => buffer.toString())