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.Instrumentation; 22 import android.app.UiAutomation; 23 import android.os.ParcelFileDescriptor; 24 import android.provider.Settings; 25 import android.util.Log; 26 27 import androidx.test.platform.app.InstrumentationRegistry; 28 29 import com.android.bedstead.nene.TestApis; 30 import com.android.bedstead.nene.exceptions.AdbException; 31 import com.android.compatibility.common.util.FileUtils; 32 33 import java.io.BufferedReader; 34 import java.io.FileInputStream; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.io.InputStreamReader; 38 import java.util.function.Function; 39 import java.util.function.Predicate; 40 import java.util.stream.Collectors; 41 import java.util.stream.Stream; 42 43 /** 44 * Utilities for interacting with adb shell commands. 45 * 46 * <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`. 47 */ 48 public final class ShellCommandUtils { 49 50 private static final String LOG_TAG = ShellCommandUtils.class.getSimpleName(); 51 52 private static final int OUT_DESCRIPTOR_INDEX = 0; 53 private static final int IN_DESCRIPTOR_INDEX = 1; 54 private static final int ERR_DESCRIPTOR_INDEX = 2; 55 56 private static final TestApis sTestApis = new TestApis(); 57 58 private static final boolean SHOULD_LOG = shouldLog(); 59 shouldLog()60 private static boolean shouldLog() { 61 try { 62 return Settings.Global.getInt( 63 sTestApis.context().instrumentedContext().getContentResolver(), 64 "nene_log") == 1; 65 } catch (Settings.SettingNotFoundException e) { 66 return false; 67 } 68 } 69 ShellCommandUtils()70 private ShellCommandUtils() { } 71 72 /** 73 * Execute an adb shell command. 74 * 75 * <p>When running on S and above, any failures in executing the command will result in an 76 * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException} 77 * will be thrown when the command returns no output (indicating that there is an error on 78 * stderr which cannot be read by this method) but some failures will return seemingly correctly 79 * but with an error in the returned string. 80 * 81 * <p>Callers should be careful to check the command's output is valid. 82 */ executeCommand(String command)83 static String executeCommand(String command) throws AdbException { 84 return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null); 85 } 86 87 /** 88 * Wraps executeShellCommandRwe to suppress NewApi warning for this method in isolation. 89 * 90 * This method was changed from TestApi -> public for API 34, so it's safe to call back to 91 * API 29, but the NewApi warning doesn't understand this. 92 */ 93 @SuppressWarnings("NewApi") // executeShellCommandRwe was @TestApi back to API 29, now public executeShellCommandRwe(String command)94 private static ParcelFileDescriptor[] executeShellCommandRwe(String command) { 95 return uiAutomation().executeShellCommandRwe(command); 96 } 97 98 /** 99 * Execute a shell command and receive a stream of lines. 100 * 101 * <p>Note that this will not deal with errors in the output. 102 * 103 * <p>Make sure you close the returned {@link StreamingShellOutput} after reading. 104 */ executeCommandForStream(String command, byte[] stdInBytes)105 public static StreamingShellOutput executeCommandForStream(String command, byte[] stdInBytes) 106 throws AdbException { 107 try { 108 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 109 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 110 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 111 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 112 113 writeStdInAndClose(fdIn, stdInBytes); 114 fdErr.close(); 115 116 FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut); 117 BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); 118 119 return new StreamingShellOutput(fis, reader.lines()); 120 } catch (IOException e) { 121 throw new AdbException("Error executing command", command, e); 122 } 123 } 124 executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)125 static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) 126 throws AdbException { 127 logCommand(command, allowEmptyOutput, stdInBytes); 128 129 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 130 return executeCommandPreS(command, allowEmptyOutput, stdInBytes); 131 } 132 133 try { 134 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 135 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 136 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 137 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 138 139 writeStdInAndClose(fdIn, stdInBytes); 140 141 String out = new String(readStreamAndClose(fdOut)); 142 String err = new String(readStreamAndClose(fdErr)); 143 144 if (!err.isEmpty()) { 145 throw new AdbException("Error executing command", command, out, err); 146 } 147 148 if (SHOULD_LOG) { 149 Log.d(LOG_TAG, "Command result: " + out); 150 } 151 152 return out; 153 } catch (IOException e) { 154 throw new AdbException("Error executing command", command, e); 155 } 156 } 157 executeCommandForBytes(String command)158 static byte[] executeCommandForBytes(String command) throws AdbException { 159 return executeCommandForBytes(command, /* stdInBytes= */ null); 160 } 161 executeCommandForBytes(String command, byte[] stdInBytes)162 static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException { 163 logCommand(command, /* allowEmptyOutput= */ false, stdInBytes); 164 165 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 166 return executeCommandForBytesPreS(command, stdInBytes); 167 } 168 169 // TODO(scottjonathan): Add argument to force errors to stderr 170 try { 171 172 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 173 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 174 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 175 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 176 177 writeStdInAndClose(fdIn, stdInBytes); 178 179 byte[] out = readStreamAndClose(fdOut); 180 String err = new String(readStreamAndClose(fdErr)); 181 182 if (!err.isEmpty()) { 183 throw new AdbException("Error executing command", command, err); 184 } 185 186 return out; 187 } catch (IOException e) { 188 throw new AdbException("Error executing command", command, e); 189 } 190 } 191 logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)192 private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) { 193 if (!SHOULD_LOG) { 194 return; 195 } 196 197 StringBuilder logBuilder = new StringBuilder("Executing shell command "); 198 logBuilder.append(command); 199 if (allowEmptyOutput) { 200 logBuilder.append(" (allow empty output)"); 201 } 202 if (stdInBytes != null) { 203 logBuilder.append(" (writing to stdIn)"); 204 } 205 Log.d(LOG_TAG, logBuilder.toString()); 206 } 207 208 /** 209 * Execute an adb shell command and check that the output meets a given criteria. 210 * 211 * <p>On S and above, any output printed to standard error will result in an exception and the 212 * {@code outputSuccessChecker} not being called. Empty output will still be processed. 213 * 214 * <p>Prior to S, if there is no output on standard out, regardless of if there is output on 215 * standard error, {@code outputSuccessChecker} will not be called. 216 * 217 * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the 218 * command executed successfully. 219 */ executeCommandAndValidateOutput( String command, Function<String, Boolean> outputSuccessChecker)220 static String executeCommandAndValidateOutput( 221 String command, Function<String, Boolean> outputSuccessChecker) throws AdbException { 222 return executeCommandAndValidateOutput(command, 223 /* allowEmptyOutput= */ false, 224 /* stdInBytes= */ null, 225 outputSuccessChecker); 226 } 227 executeCommandAndValidateOutput( String command, boolean allowEmptyOutput, byte[] stdInBytes, Function<String, Boolean> outputSuccessChecker)228 static String executeCommandAndValidateOutput( 229 String command, 230 boolean allowEmptyOutput, 231 byte[] stdInBytes, 232 Function<String, Boolean> outputSuccessChecker) throws AdbException { 233 String output = executeCommand(command, allowEmptyOutput, stdInBytes); 234 if (!outputSuccessChecker.apply(output)) { 235 throw new AdbException("Command did not meet success criteria", command, output); 236 } 237 return output; 238 } 239 240 /** 241 * Return {@code true} if {@code output} starts with "success", case insensitive. 242 */ startsWithSuccess(String output)243 public static boolean startsWithSuccess(String output) { 244 return output.toUpperCase().startsWith("SUCCESS"); 245 } 246 247 /** 248 * Return {@code true} if {@code output} does not start with "error", case insensitive. 249 */ doesNotStartWithError(String output)250 public static boolean doesNotStartWithError(String output) { 251 return !output.toUpperCase().startsWith("ERROR"); 252 } 253 254 @SuppressWarnings("NewApi") executeCommandPreS( String command, boolean allowEmptyOutput, byte[] stdIn)255 private static String executeCommandPreS( 256 String command, boolean allowEmptyOutput, byte[] stdIn) throws AdbException { 257 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command); 258 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 259 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 260 261 try { 262 writeStdInAndClose(fdIn, stdIn); 263 264 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 265 String out = new String(FileUtils.readInputStreamFully(fis)); 266 267 if (!allowEmptyOutput && out.isEmpty()) { 268 throw new AdbException( 269 "No output from command. There's likely an error on stderr", 270 command, out); 271 } 272 273 if (SHOULD_LOG) { 274 Log.d(LOG_TAG, "Command result: " + out); 275 } 276 277 return out; 278 } 279 } catch (IOException e) { 280 throw new AdbException( 281 "Error reading command output", command, e); 282 } 283 } 284 285 // This is warned for executeShellCommandRw which did exist as TestApi 286 @SuppressWarnings("NewApi") executeCommandForBytesPreS( String command, byte[] stdInBytes)287 private static byte[] executeCommandForBytesPreS( 288 String command, byte[] stdInBytes) throws AdbException { 289 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command); 290 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 291 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 292 293 try { 294 writeStdInAndClose(fdIn, stdInBytes); 295 296 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 297 return FileUtils.readInputStreamFully(fis); 298 } 299 } catch (IOException e) { 300 throw new AdbException( 301 "Error reading command output", command, e); 302 } 303 } 304 writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)305 private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes) 306 throws IOException { 307 if (stdInBytes != null) { 308 try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) { 309 fos.write(stdInBytes); 310 } 311 } else { 312 fdIn.close(); 313 } 314 } 315 readStreamAndClose(ParcelFileDescriptor fd)316 private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException { 317 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { 318 return FileUtils.readInputStreamFully(fis); 319 } 320 } 321 322 /** 323 * Get a {@link Instrumentation}. 324 */ instrumentation()325 public static Instrumentation instrumentation() { 326 return InstrumentationRegistry.getInstrumentation(); 327 } 328 329 /** 330 * Get a {@link UiAutomation}. 331 */ uiAutomation()332 public static UiAutomation uiAutomation() { 333 return instrumentation().getUiAutomation(); 334 } 335 336 /** Wrapper around {@link Stream} of lines output from a shell command. */ 337 public static final class StreamingShellOutput implements AutoCloseable { 338 339 private final FileInputStream mFileInputStream; 340 private final Stream<String> mStream; 341 StreamingShellOutput(FileInputStream fileInputStream, Stream<String> stream)342 StreamingShellOutput(FileInputStream fileInputStream, Stream<String> stream) { 343 mFileInputStream = fileInputStream; 344 mStream = stream; 345 } 346 stream()347 public Stream<String> stream() { 348 return mStream; 349 } 350 351 352 @Override close()353 public void close() throws IOException { 354 mFileInputStream.close(); 355 mStream.close(); 356 } 357 } 358 } 359