• 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 
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