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 static java.time.temporal.ChronoUnit.SECONDS; 22 23 import android.app.Instrumentation; 24 import android.app.UiAutomation; 25 import android.os.Build; 26 import android.os.ParcelFileDescriptor; 27 import android.provider.Settings; 28 import android.util.Log; 29 30 import androidx.test.platform.app.InstrumentationRegistry; 31 32 import com.android.bedstead.nene.TestApis; 33 import com.android.bedstead.nene.exceptions.AdbException; 34 35 import java.io.BufferedReader; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.io.InputStreamReader; 40 import java.time.Duration; 41 import java.util.function.Function; 42 import java.util.stream.Stream; 43 44 /** 45 * Utilities for interacting with adb shell commands. 46 * 47 * <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`. 48 */ 49 public final class ShellCommandUtils { 50 51 private static final String LOG_TAG = ShellCommandUtils.class.getSimpleName(); 52 53 private static final int OUT_DESCRIPTOR_INDEX = 0; 54 private static final int IN_DESCRIPTOR_INDEX = 1; 55 private static final int ERR_DESCRIPTOR_INDEX = 2; 56 private static final boolean SHOULD_LOG = shouldLog(); 57 shouldLog()58 private static boolean shouldLog() { 59 try { 60 return Settings.Global.getInt( 61 TestApis.context().instrumentedContext().getContentResolver(), 62 "nene_log") == 1; 63 } catch (Settings.SettingNotFoundException e) { 64 return false; 65 } 66 } 67 ShellCommandUtils()68 private ShellCommandUtils() { } 69 70 private static Boolean sRootAvailable = null; 71 private static Boolean sIsRunningAsRoot = null; 72 private static Boolean sSuperUserAvailable = null; 73 74 /** 75 * Execute an adb shell command. 76 * 77 * <p>When running on S and above, any failures in executing the command will result in an 78 * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException} 79 * will be thrown when the command returns no output (indicating that there is an error on 80 * stderr which cannot be read by this method) but some failures will return seemingly correctly 81 * but with an error in the returned string. 82 * 83 * <p>Callers should be careful to check the command's output is valid. 84 */ executeCommand(String command)85 static String executeCommand(String command) throws AdbException { 86 return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null); 87 } 88 89 /** 90 * Wraps executeShellCommandRwe to suppress NewApi warning for this method in isolation. 91 * 92 * This method was changed from TestApi -> public for API 34, so it's safe to call back to 93 * API 29, but the NewApi warning doesn't understand this. 94 */ 95 @SuppressWarnings("NewApi") // executeShellCommandRwe was @TestApi back to API 29, now public executeShellCommandRwe(String command)96 private static ParcelFileDescriptor[] executeShellCommandRwe(String command) { 97 return uiAutomation().executeShellCommandRwe(command); 98 } 99 100 /** 101 * Execute a shell command and receive a stream of lines. 102 * 103 * <p>Note that this will not deal with errors in the output. 104 * 105 * <p>Make sure you close the returned {@link StreamingShellOutput} after reading. 106 */ executeCommandForStream(String command, byte[] stdInBytes)107 public static StreamingShellOutput executeCommandForStream(String command, byte[] stdInBytes) 108 throws AdbException { 109 try { 110 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 111 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 112 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 113 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 114 115 writeStdInAndClose(fdIn, stdInBytes); 116 fdErr.close(); 117 118 FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut); 119 BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); 120 121 return new StreamingShellOutput(fis, reader.lines()); 122 } catch (IOException e) { 123 throw new AdbException("Error executing command", command, e); 124 } 125 } 126 executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)127 static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) 128 throws AdbException { 129 logCommand(command, allowEmptyOutput, stdInBytes); 130 131 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 132 return executeCommandPreS(command, allowEmptyOutput, stdInBytes); 133 } 134 135 try { 136 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 137 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 138 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 139 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 140 141 writeStdInAndClose(fdIn, stdInBytes); 142 143 String out = new String(readStreamAndClose(fdOut)); 144 String err = new String(readStreamAndClose(fdErr)); 145 146 if (!err.isEmpty()) { 147 throw new AdbException("Error executing command", command, out, err); 148 } 149 150 if (SHOULD_LOG) { 151 Log.d(LOG_TAG, "Command result: " + out); 152 } 153 154 return out; 155 } catch (IOException e) { 156 throw new AdbException("Error executing command", command, e); 157 } 158 } 159 executeCommandForBytes(String command)160 static byte[] executeCommandForBytes(String command) throws AdbException { 161 return executeCommandForBytes(command, /* stdInBytes= */ null); 162 } 163 executeCommandForBytes(String command, byte[] stdInBytes)164 static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException { 165 logCommand(command, /* allowEmptyOutput= */ false, stdInBytes); 166 167 if (!Versions.meetsMinimumSdkVersionRequirement(S)) { 168 return executeCommandForBytesPreS(command, stdInBytes); 169 } 170 171 // TODO(scottjonathan): Add argument to force errors to stderr 172 try { 173 174 ParcelFileDescriptor[] fds = executeShellCommandRwe(command); 175 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 176 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 177 ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX]; 178 179 writeStdInAndClose(fdIn, stdInBytes); 180 181 byte[] out = readStreamAndClose(fdOut); 182 String err = new String(readStreamAndClose(fdErr)); 183 184 if (!err.isEmpty()) { 185 throw new AdbException("Error executing command", command, err); 186 } 187 188 return out; 189 } catch (IOException e) { 190 throw new AdbException("Error executing command", command, e); 191 } 192 } 193 logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)194 private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) { 195 if (!SHOULD_LOG) { 196 return; 197 } 198 199 StringBuilder logBuilder = new StringBuilder("Executing shell command "); 200 logBuilder.append(command); 201 if (allowEmptyOutput) { 202 logBuilder.append(" (allow empty output)"); 203 } 204 if (stdInBytes != null) { 205 logBuilder.append(" (writing to stdIn)"); 206 } 207 Log.d(LOG_TAG, logBuilder.toString()); 208 } 209 210 /** 211 * Execute an adb shell command and check that the output meets a given criteria. 212 * 213 * <p>On S and above, any output printed to standard error will result in an exception and the 214 * {@code outputSuccessChecker} not being called. Empty output will still be processed. 215 * 216 * <p>Prior to S, if there is no output on standard out, regardless of if there is output on 217 * standard error, {@code outputSuccessChecker} will not be called. 218 * 219 * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the 220 * command executed successfully. 221 */ executeCommandAndValidateOutput( String command, Function<String, Boolean> outputSuccessChecker)222 static String executeCommandAndValidateOutput( 223 String command, Function<String, Boolean> outputSuccessChecker) throws AdbException { 224 return executeCommandAndValidateOutput(command, 225 /* allowEmptyOutput= */ false, 226 /* stdInBytes= */ null, 227 outputSuccessChecker); 228 } 229 executeCommandAndValidateOutput( String command, boolean allowEmptyOutput, byte[] stdInBytes, Function<String, Boolean> outputSuccessChecker)230 static String executeCommandAndValidateOutput( 231 String command, 232 boolean allowEmptyOutput, 233 byte[] stdInBytes, 234 Function<String, Boolean> outputSuccessChecker) throws AdbException { 235 String output = executeCommand(command, allowEmptyOutput, stdInBytes); 236 if (!outputSuccessChecker.apply(output)) { 237 throw new AdbException("Command did not meet success criteria", command, output); 238 } 239 return output; 240 } 241 242 /** 243 * Return {@code true} if {@code output} starts with "success", case insensitive. 244 */ startsWithSuccess(String output)245 public static boolean startsWithSuccess(String output) { 246 return output.toUpperCase().startsWith("SUCCESS"); 247 } 248 249 /** 250 * Return {@code true} if {@code output} does not start with "error", case insensitive. 251 */ doesNotStartWithError(String output)252 public static boolean doesNotStartWithError(String output) { 253 return !output.toUpperCase().startsWith("ERROR"); 254 } 255 256 @SuppressWarnings("NewApi") executeCommandPreS( String command, boolean allowEmptyOutput, byte[] stdIn)257 private static String executeCommandPreS( 258 String command, boolean allowEmptyOutput, byte[] stdIn) throws AdbException { 259 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command); 260 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 261 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 262 263 try { 264 writeStdInAndClose(fdIn, stdIn); 265 266 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 267 String out = new String(FileUtils.readInputStreamFully(fis)); 268 269 if (!allowEmptyOutput && out.isEmpty()) { 270 throw new AdbException( 271 "No output from command. There's likely an error on stderr", 272 command, out); 273 } 274 275 if (SHOULD_LOG) { 276 Log.d(LOG_TAG, "Command result: " + out); 277 } 278 279 return out; 280 } 281 } catch (IOException e) { 282 throw new AdbException( 283 "Error reading command output", command, e); 284 } 285 } 286 287 // This is warned for executeShellCommandRw which did exist as TestApi 288 @SuppressWarnings("NewApi") executeCommandForBytesPreS( String command, byte[] stdInBytes)289 private static byte[] executeCommandForBytesPreS( 290 String command, byte[] stdInBytes) throws AdbException { 291 ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command); 292 ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX]; 293 ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX]; 294 295 try { 296 writeStdInAndClose(fdIn, stdInBytes); 297 298 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) { 299 return FileUtils.readInputStreamFully(fis); 300 } 301 } catch (IOException e) { 302 throw new AdbException( 303 "Error reading command output", command, e); 304 } 305 } 306 writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)307 private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes) 308 throws IOException { 309 if (stdInBytes != null) { 310 try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) { 311 fos.write(stdInBytes); 312 } 313 } else { 314 fdIn.close(); 315 } 316 } 317 readStreamAndClose(ParcelFileDescriptor fd)318 private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException { 319 try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { 320 return FileUtils.readInputStreamFully(fis); 321 } 322 } 323 324 /** 325 * Get a {@link Instrumentation}. 326 */ instrumentation()327 public static Instrumentation instrumentation() { 328 return InstrumentationRegistry.getInstrumentation(); 329 } 330 331 /** 332 * Get a {@link UiAutomation}. 333 */ uiAutomation()334 public static UiAutomation uiAutomation() { 335 return instrumentation().getUiAutomation(); 336 } 337 isSuperUserAvailable()338 public static boolean isSuperUserAvailable() { 339 if (sSuperUserAvailable != null) { 340 return sSuperUserAvailable; 341 } 342 343 try { 344 // We run a basic command to check if the device can use the super user. 345 // Don't use .asRoot() here as it will cause infinite recursion, or add/keep the timeout 346 //TODO(b/301478821): Remove the timeout once b/303377922 is fixed. 347 String output = ShellCommand.builder("su root echo hello") 348 .withTimeout(Duration.of(1, SECONDS)).execute(); 349 if (output.contains("hello")) { 350 sSuperUserAvailable = true; 351 } 352 } catch (AdbException e) { 353 Log.i(LOG_TAG, "Exception when checking for super user.", e); 354 } 355 356 if (sSuperUserAvailable == null) { 357 Log.i(LOG_TAG, 358 "Unable to run shell commands with super user as the device does not " + 359 "allow that. The device is of type: " + Build.TYPE + ".\n However, " + 360 "root may still be available. You can check with " + 361 "ShellCommandUtils.isRootAvailable."); 362 sSuperUserAvailable = false; 363 } 364 365 return sSuperUserAvailable; 366 } 367 368 /** 369 * Check if the test instrumentation is running as root. 370 */ isRunningAsRoot()371 public static boolean isRunningAsRoot() { 372 if (sIsRunningAsRoot != null) { 373 return sIsRunningAsRoot; 374 } 375 376 try { 377 // We run a basic command to check if the device is running as root. 378 // If the command can be executed without the su root prefix, the device is running 379 // as root. 380 String output = ShellCommand.builder("cat /system/build.prop") 381 .withTimeout(Duration.of(1, SECONDS)).execute(); 382 System.out.println("output >> " + output); 383 if (output.contains("ro.build")) { 384 sIsRunningAsRoot = true; 385 } 386 } catch (AdbException e) { 387 Log.i(LOG_TAG, "Exception when checking if test instrumentation is running as root.", e); 388 } 389 390 if (sIsRunningAsRoot == null) { 391 Log.i(LOG_TAG, 392 "Unable to run shell commands as root without the su root prefix. " + 393 "The device is of type: " + Build.TYPE + ".\n However, the " + 394 "super user may be available. You can check with " + 395 "ShellCommandUtils.isRootAvailable."); 396 sIsRunningAsRoot = false; 397 } 398 399 return sIsRunningAsRoot; 400 } 401 402 /** 403 * Check if the device can run commands as root. 404 */ isRootAvailable()405 public static boolean isRootAvailable() { 406 if (sRootAvailable != null) { 407 return sRootAvailable; 408 } 409 410 if (isRunningAsRoot() || canRunAsRootWithSuperUser()) { 411 sRootAvailable = true; 412 } 413 414 if (sRootAvailable == null) { 415 Log.i(LOG_TAG, 416 "Unable to run the test as root as the device does not allow that. " 417 + "The device is of type: " + Build.TYPE); 418 sRootAvailable = false; 419 } 420 421 return sRootAvailable; 422 } 423 canRunAsRootWithSuperUser()424 private static boolean canRunAsRootWithSuperUser() { 425 try { 426 // We run a basic command to check if the device can run it as root. 427 //TODO(b/301478821): Remove the timeout once b/303377922 is fixed. 428 String output = ShellCommand.builder("cat /system/build.prop").asRoot(true) 429 .withTimeout(Duration.of(1, SECONDS)).execute(); 430 if (output.contains("ro.build")) { 431 return true; 432 } 433 } catch (AdbException e) { 434 Log.i(LOG_TAG, "Exception when checking for super user.", e); 435 } 436 return false; 437 } 438 439 /** Wrapper around {@link Stream} of lines output from a shell command. */ 440 public static final class StreamingShellOutput implements AutoCloseable { 441 442 private final FileInputStream mFileInputStream; 443 private final Stream<String> mStream; 444 StreamingShellOutput(FileInputStream fileInputStream, Stream<String> stream)445 StreamingShellOutput(FileInputStream fileInputStream, Stream<String> stream) { 446 mFileInputStream = fileInputStream; 447 mStream = stream; 448 } 449 stream()450 public Stream<String> stream() { 451 return mStream; 452 } 453 454 455 @Override close()456 public void close() throws IOException { 457 mFileInputStream.close(); 458 mStream.close(); 459 } 460 } 461 } 462