Organise files per device
Summary: Moved all logic per device type we support to its own dir, including tools and utilities around it, which makes it easier to consolidate logic and decouple in turn per device type. Per type, all logic can be found in `server/devices/(desktop|metro|android|ios|webapp)` Reviewed By: timur-valiev Differential Revision: D30277817 fbshipit-source-id: 2b5339c363d5d31ceeba07cec03826fc67cf3748
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6175424d16
commit
c0cd32564a
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is intentionally separate from androidContainerUtility so the
|
||||
* opaque types will ensure the commands are only ever run on validated
|
||||
* arguments.
|
||||
*/
|
||||
import {UnsupportedError} from '../../../utils/metrics';
|
||||
import adbkit, {Client} from 'adbkit';
|
||||
|
||||
const allowedAppNameRegex = /^[\w.-]+$/;
|
||||
const appNotApplicationRegex = /not an application/;
|
||||
const appNotDebuggableRegex = /debuggable/;
|
||||
const operationNotPermittedRegex = /not permitted/;
|
||||
const logTag = 'androidContainerUtility';
|
||||
|
||||
export type AppName = string;
|
||||
export type Command = string;
|
||||
export type FilePath = string;
|
||||
export type FileContent = string;
|
||||
|
||||
export function validateAppName(app: string): Promise<AppName> {
|
||||
if (app.match(allowedAppNameRegex)) {
|
||||
return Promise.resolve(app);
|
||||
}
|
||||
return Promise.reject(new Error(`Disallowed run-as user: ${app}`));
|
||||
}
|
||||
|
||||
export function validateFilePath(filePath: string): Promise<FilePath> {
|
||||
if (!filePath.match(/[']/)) {
|
||||
return Promise.resolve(filePath);
|
||||
}
|
||||
return Promise.reject(new Error(`Disallowed escaping filepath: ${filePath}`));
|
||||
}
|
||||
|
||||
export function validateFileContent(content: string): Promise<FileContent> {
|
||||
if (!content.match(/["]/)) {
|
||||
return Promise.resolve(content);
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(`Disallowed escaping file content: ${content}`),
|
||||
);
|
||||
}
|
||||
|
||||
enum RunAsErrorCode {
|
||||
NotAnApp = 1,
|
||||
NotDebuggable = 2,
|
||||
}
|
||||
|
||||
class RunAsError extends Error {
|
||||
code: RunAsErrorCode;
|
||||
|
||||
constructor(code: RunAsErrorCode, message?: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export function _push(
|
||||
client: Client,
|
||||
deviceId: string,
|
||||
app: AppName,
|
||||
filename: FilePath,
|
||||
contents: FileContent,
|
||||
): Promise<void> {
|
||||
console.debug(`Deploying ${filename} to ${deviceId}:${app}`, logTag);
|
||||
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.
|
||||
return executeCommandWithSu(client, deviceId, app, command)
|
||||
.then((_) => undefined)
|
||||
.catch((e) => {
|
||||
console.debug(e);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function _pull(
|
||||
client: Client,
|
||||
deviceId: string,
|
||||
app: AppName,
|
||||
path: FilePath,
|
||||
): Promise<string> {
|
||||
const command = `cat '${path}'`;
|
||||
return executeCommandAsApp(client, deviceId, app, command).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).catch((e) => {
|
||||
// Throw the original error.
|
||||
console.debug(e);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Keep this method private since it relies on pre-validated arguments
|
||||
function executeCommandAsApp(
|
||||
client: Client,
|
||||
deviceId: string,
|
||||
app: string,
|
||||
command: string,
|
||||
): Promise<string> {
|
||||
return _executeCommandWithRunner(
|
||||
client,
|
||||
deviceId,
|
||||
app,
|
||||
command,
|
||||
`run-as '${app}'`,
|
||||
);
|
||||
}
|
||||
|
||||
function executeCommandWithSu(
|
||||
client: Client,
|
||||
deviceId: string,
|
||||
app: string,
|
||||
command: string,
|
||||
): Promise<string> {
|
||||
return _executeCommandWithRunner(client, deviceId, app, command, 'su');
|
||||
}
|
||||
|
||||
function _executeCommandWithRunner(
|
||||
client: Client,
|
||||
deviceId: string,
|
||||
app: string,
|
||||
command: string,
|
||||
runner: string,
|
||||
): Promise<string> {
|
||||
return client
|
||||
.shell(deviceId, `echo '${command}' | ${runner}`)
|
||||
.then(adbkit.util.readAll)
|
||||
.then((buffer) => buffer.toString())
|
||||
.then((output) => {
|
||||
if (output.match(appNotApplicationRegex)) {
|
||||
throw new RunAsError(
|
||||
RunAsErrorCode.NotAnApp,
|
||||
`Android package ${app} is not an application. To use it with Flipper, either run adb as root or add an <application> tag to AndroidManifest.xml`,
|
||||
);
|
||||
}
|
||||
if (output.match(appNotDebuggableRegex)) {
|
||||
throw new RunAsError(
|
||||
RunAsErrorCode.NotDebuggable,
|
||||
`Android app ${app} is not debuggable. To use it with Flipper, add android:debuggable="true" to the application section of AndroidManifest.xml`,
|
||||
);
|
||||
}
|
||||
if (output.toLowerCase().match(operationNotPermittedRegex)) {
|
||||
throw new UnsupportedError(
|
||||
`Your android device (${deviceId}) does not support the adb shell run-as command. We're tracking this at https://github.com/facebook/flipper/issues/92`,
|
||||
);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user