blob: 3a624f3396463aaf5977200d42aed2e23fb1c046 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bedstead.nene.utils;
import static android.os.Build.VERSION_CODES.S;
import android.app.UiAutomation;
import android.os.ParcelFileDescriptor;
import android.provider.Settings;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.compatibility.common.util.FileUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.function.Function;
/**
* Utilities for interacting with adb shell commands.
*
* <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`.
*/
public final class ShellCommandUtils {
private static final String LOG_TAG = ShellCommandUtils.class.getName();
private static final int OUT_DESCRIPTOR_INDEX = 0;
private static final int IN_DESCRIPTOR_INDEX = 1;
private static final int ERR_DESCRIPTOR_INDEX = 2;
private static final TestApis sTestApis = new TestApis();
private static final boolean SHOULD_LOG = shouldLog();
private static boolean shouldLog() {
try {
return Settings.Global.getInt(
sTestApis.context().instrumentedContext().getContentResolver(),
"nene_log") == 1;
} catch (Settings.SettingNotFoundException e) {
return false;
}
}
private ShellCommandUtils() { }
/**
* Execute an adb shell command.
*
* <p>When running on S and above, any failures in executing the command will result in an
* {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException}
* will be thrown when the command returns no output (indicating that there is an error on
* stderr which cannot be read by this method) but some failures will return seemingly correctly
* but with an error in the returned string.
*
* <p>Callers should be careful to check the command's output is valid.
*/
static String executeCommand(String command) throws AdbException {
return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null);
}
static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)
throws AdbException {
logCommand(command, allowEmptyOutput, stdInBytes);
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
return executeCommandPreS(command, allowEmptyOutput, stdInBytes);
}
// TODO(scottjonathan): Add argument to force errors to stderr
try {
ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
writeStdInAndClose(fdIn, stdInBytes);
String out = new String(readStreamAndClose(fdOut));
String err = new String(readStreamAndClose(fdErr));
if (!err.isEmpty()) {
throw new AdbException("Error executing command", command, out, err);
}
if (SHOULD_LOG) {
Log.d(LOG_TAG, "Command result: " + out);
}
return out;
} catch (IOException e) {
throw new AdbException("Error executing command", command, e);
}
}
static byte[] executeCommandForBytes(String command) throws AdbException {
return executeCommandForBytes(command, /* stdInBytes= */ null);
}
static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException {
logCommand(command, /* allowEmptyOutput= */ false, stdInBytes);
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
return executeCommandForBytesPreS(command, stdInBytes);
}
// TODO(scottjonathan): Add argument to force errors to stderr
try {
ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
writeStdInAndClose(fdIn, stdInBytes);
byte[] out = readStreamAndClose(fdOut);
String err = new String(readStreamAndClose(fdErr));
if (!err.isEmpty()) {
throw new AdbException("Error executing command", command, err);
}
return out;
} catch (IOException e) {
throw new AdbException("Error executing command", command, e);
}
}
private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) {
if (!SHOULD_LOG) {
return;
}
StringBuilder logBuilder = new StringBuilder("Executing shell command ");
logBuilder.append(command);
if (allowEmptyOutput) {
logBuilder.append(" (allow empty output)");
}
if (stdInBytes != null) {
logBuilder.append(" (writing to stdIn)");
}
Log.d(LOG_TAG, logBuilder.toString());
}
/**
* Execute an adb shell command and check that the output meets a given criteria.
*
* <p>On S and above, any output printed to standard error will result in an exception and the
* {@code outputSuccessChecker} not being called. Empty output will still be processed.
*
* <p>Prior to S, if there is no output on standard out, regardless of if there is output on
* standard error, {@code outputSuccessChecker} will not be called.
*
* <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
* command executed successfully.
*/
static String executeCommandAndValidateOutput(
String command, Function<String, Boolean> outputSuccessChecker) throws AdbException {
return executeCommandAndValidateOutput(command,
/* allowEmptyOutput= */ false,
/* stdInBytes= */ null,
outputSuccessChecker);
}
static String executeCommandAndValidateOutput(
String command,
boolean allowEmptyOutput,
byte[] stdInBytes,
Function<String, Boolean> outputSuccessChecker) throws AdbException {
String output = executeCommand(command, allowEmptyOutput, stdInBytes);
if (!outputSuccessChecker.apply(output)) {
throw new AdbException("Command did not meet success criteria", command, output);
}
return output;
}
/**
* Return {@code true} if {@code output} starts with "success", case insensitive.
*/
public static boolean startsWithSuccess(String output) {
return output.toUpperCase().startsWith("SUCCESS");
}
/**
* Return {@code true} if {@code output} does not start with "error", case insensitive.
*/
public static boolean doesNotStartWithError(String output) {
return !output.toUpperCase().startsWith("ERROR");
}
private static String executeCommandPreS(
String command, boolean allowEmptyOutput, byte[] stdIn) throws AdbException {
ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);
ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
try {
writeStdInAndClose(fdIn, stdIn);
try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
String out = new String(FileUtils.readInputStreamFully(fis));
if (!allowEmptyOutput && out.isEmpty()) {
throw new AdbException(
"No output from command. There's likely an error on stderr",
command, out);
}
if (SHOULD_LOG) {
Log.d(LOG_TAG, "Command result: " + out);
}
return out;
}
} catch (IOException e) {
throw new AdbException(
"Error reading command output", command, e);
}
}
// This is warned for executeShellCommandRw which did exist as TestApi
@SuppressWarnings("NewApi")
private static byte[] executeCommandForBytesPreS(
String command, byte[] stdInBytes) throws AdbException {
ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);
ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
try {
writeStdInAndClose(fdIn, stdInBytes);
try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
return FileUtils.readInputStreamFully(fis);
}
} catch (IOException e) {
throw new AdbException(
"Error reading command output", command, e);
}
}
private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)
throws IOException {
if (stdInBytes != null) {
try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) {
fos.write(stdInBytes);
}
} else {
fdIn.close();
}
}
private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException {
try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
return FileUtils.readInputStreamFully(fis);
}
}
/**
* Get a {@link UiAutomation}.
*/
public static UiAutomation uiAutomation() {
return InstrumentationRegistry.getInstrumentation().getUiAutomation();
}
}