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