• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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