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