1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bedstead.nene.utils; 18 19 import static android.os.Build.VERSION_CODES.S; 20 21 import android.app.UiAutomation; 22 import android.os.ParcelFileDescriptor; 23 import android.provider.Settings; 24 import android.util.Log; 25 26 import androidx.test.platform.app.InstrumentationRegistry; 27 28 import com.android.bedstead.nene.TestApis; 29 import com.android.bedstead.nene.exceptions.AdbException; 30 import com.android.compatibility.common.util.FileUtils; 31 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.util.function.Function; 36 37 /** 38 * Utilities for interacting with adb shell commands. 39 * 40 * <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`. 41 */ 42 public final class ShellCommandUtils { 43 44 private static final String LOG_TAG = ShellCommandUtils.class.getName(); 45 46 private static final int OUT_DESCRIPTOR_INDEX = 0; 47 private static final int IN_DESCRIPTOR_INDEX = 1; 48 private static final int ERR_DESCRIPTOR_INDEX = 2; 49 50 private static final TestApis sTestApis = new TestApis(); 51 52 private static final boolean SHOULD_LOG = shouldLog(); 53 shouldLog()54 private static boolean shouldLog() { 55 try { 56 return Settings.Global.getInt( 57 sTestApis.context().instrumentedContext().getContentResolver(), 58 "nene_log") == 1; 59 } catch (Settings.SettingNotFoundException e) { 60 return false; 61 } 62 } 63 ShellCommandUtils()64 private ShellCommandUtils() { } 65 66 /** 67 * Execute an adb shell command. 68 * 69 * <p>When running on S and above, any failures in executing the command will result in an 70 * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException} 71 * will be thrown when the command returns no output (indicating that there is an error on 72 * stderr which cannot be read by this method) but some failures will return seemingly correctly 73 * but with an error in the returned string. 74 * 75 * <p>Callers should be careful to check the command's output is valid. 76 */ executeCommand(String command)77 static String executeCommand(String command) throws AdbException { 78 return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null); 79 } 80 executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)81 static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) 82 throws AdbException { 83 logCommand(command, allowEmptyOutput, stdInBytes); 84 85 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 86 if (stdInBytes != null && stdInBytes.length > 0) { 87 throw new IllegalStateException("Cannot write to stdIn prior to S"); 88 } 89 return executeCommandPreS(command, allowEmptyOutput); 90 } 91 92 // TODO(scottjonathan): Add argument to force errors to stderr 93 try { 94 95 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command); 96 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 97 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 98 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 99 100 writeStdInAndClose(fdIn, stdInBytes); 101 102 String out = new String(readStreamAndClose(fdOut)); 103 String err = new String(readStreamAndClose(fdErr)); 104 105 if (!err.isEmpty()) { 106 throw new AdbException("Error executing command", command, out, err); 107 } 108 109 if (SHOULD_LOG) { 110 Log.d(LOG_TAG, "Command result: " + out); 111 } 112 113 return out; 114 } catch (IOException e) { 115 throw new AdbException("Error executing command", command, e); 116 } 117 } 118 executeCommandForBytes(String command)119 static byte[] executeCommandForBytes(String command) throws AdbException { 120 return executeCommandForBytes(command, /* stdInBytes= */ null); 121 } 122 executeCommandForBytes(String command, byte[] stdInBytes)123 static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException { 124 logCommand(command, /* allowEmptyOutput= */ false, stdInBytes); 125 126 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 127 if (stdInBytes != null && stdInBytes.length > 0) { 128 throw new IllegalStateException("Cannot write to stdIn prior to S"); 129 } 130 131 return executeCommandForBytesPreS(command); 132 } 133 134 // TODO(scottjonathan): Add argument to force errors to stderr 135 try { 136 137 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command); 138 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 139 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 140 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 141 142 writeStdInAndClose(fdIn, stdInBytes); 143 144 byte[] out = readStreamAndClose(fdOut); 145 String err = new String(readStreamAndClose(fdErr)); 146 147 if (!err.isEmpty()) { 148 throw new AdbException("Error executing command", command, err); 149 } 150 151 return out; 152 } catch (IOException e) { 153 throw new AdbException("Error executing command", command, e); 154 } 155 } 156 logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)157 private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) { 158 if (!SHOULD_LOG) { 159 return; 160 } 161 162 StringBuilder logBuilder = new StringBuilder("Executing shell command "); 163 logBuilder.append(command); 164 if (allowEmptyOutput) { 165 logBuilder.append(" (allow empty output)"); 166 } 167 if (stdInBytes != null) { 168 logBuilder.append(" (writing to stdIn)"); 169 } 170 Log.d(LOG_TAG, logBuilder.toString()); 171 } 172 173 /** 174 * Execute an adb shell command and check that the output meets a given criteria. 175 * 176 * <p>On S and above, any output printed to standard error will result in an exception and the 177 * {@code outputSuccessChecker} not being called. Empty output will still be processed. 178 * 179 * <p>Prior to S, if there is no output on standard out, regardless of if there is output on 180 * standard error, {@code outputSuccessChecker} will not be called. 181 * 182 * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the 183 * command executed successfully. 184 */ executeCommandAndValidateOutput( String command, Function<String, Boolean> outputSuccessChecker)185 static String executeCommandAndValidateOutput( 186 String command, Function<String, Boolean> outputSuccessChecker) throws AdbException { 187 return executeCommandAndValidateOutput(command, 188 /* allowEmptyOutput= */ false, 189 /* stdInBytes= */ null, 190 outputSuccessChecker); 191 } 192 executeCommandAndValidateOutput( String command, boolean allowEmptyOutput, byte[] stdInBytes, Function<String, Boolean> outputSuccessChecker)193 static String executeCommandAndValidateOutput( 194 String command, 195 boolean allowEmptyOutput, 196 byte[] stdInBytes, 197 Function<String, Boolean> outputSuccessChecker) throws AdbException { 198 String output = executeCommand(command, allowEmptyOutput, stdInBytes); 199 if (!outputSuccessChecker.apply(output)) { 200 throw new AdbException("Command did not meet success criteria", command, output); 201 } 202 return output; 203 } 204 205 /** 206 * Return {@code true} if {@code output} starts with "success", case insensitive. 207 */ startsWithSuccess(String output)208 public static boolean startsWithSuccess(String output) { 209 return output.toUpperCase().startsWith("SUCCESS"); 210 } 211 212 /** 213 * Return {@code true} if {@code output} does not start with "error", case insensitive. 214 */ doesNotStartWithError(String output)215 public static boolean doesNotStartWithError(String output) { 216 return !output.toUpperCase().startsWith("ERROR"); 217 } 218 executeCommandPreS( String command, boolean allowEmptyOutput)219 private static String executeCommandPreS( 220 String command, boolean allowEmptyOutput) throws AdbException { 221 ParcelFileDescriptor fdOut = uiAutomation().executeShellCommand(command); 222 223 try { 224 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 225 String out = new String(FileUtils.readInputStreamFully(fis)); 226 227 if (!allowEmptyOutput && out.isEmpty()) { 228 throw new AdbException( 229 "No output from command. There's likely an error on stderr", 230 command, out); 231 } 232 233 if (SHOULD_LOG) { 234 Log.d(LOG_TAG, "Command result: " + out); 235 } 236 237 return out; 238 } 239 } catch (IOException e) { 240 throw new AdbException( 241 "Error reading command output", command, e); 242 } 243 } 244 executeCommandForBytesPreS(String command)245 private static byte[] executeCommandForBytesPreS(String command) throws AdbException { 246 ParcelFileDescriptor fdOut = uiAutomation().executeShellCommand(command); 247 248 try { 249 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 250 return FileUtils.readInputStreamFully(fis); 251 } 252 } catch (IOException e) { 253 throw new AdbException( 254 "Error reading command output", command, e); 255 } 256 } 257 writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)258 private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes) 259 throws IOException { 260 if (stdInBytes != null) { 261 try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) { 262 fos.write(stdInBytes); 263 } 264 } else { 265 fdIn.close(); 266 } 267 } 268 readStreamAndClose(ParcelFileDescriptor fd)269 private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException { 270 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { 271 return FileUtils.readInputStreamFully(fis); 272 } 273 } 274 275 /** 276 * Get a {@link UiAutomation}. 277 */ uiAutomation()278 public static UiAutomation uiAutomation() { 279 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 280 } 281 } 282