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.UiAutomation;
22 import android.os.ParcelFileDescriptor;
23 import android.provider.Settings;
24 import android.util.Log;
25 
26 import androidx.test.platform.app.InstrumentationRegistry;
27 
28 import com.android.bedstead.nene.TestApis;
29 import com.android.bedstead.nene.exceptions.AdbException;
30 import com.android.compatibility.common.util.FileUtils;
31 
32 import java.io.FileInputStream;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.util.function.Function;
36 
37 /**
38  * Utilities for interacting with adb shell commands.
39  *
40  * <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`.
41  */
42 public final class ShellCommandUtils {
43 
44     private static final String LOG_TAG = ShellCommandUtils.class.getName();
45 
46     private static final int OUT_DESCRIPTOR_INDEX = 0;
47     private static final int IN_DESCRIPTOR_INDEX = 1;
48     private static final int ERR_DESCRIPTOR_INDEX = 2;
49 
50     private static final TestApis sTestApis = new TestApis();
51 
52     private static final boolean SHOULD_LOG = shouldLog();
53 
shouldLog()54     private static boolean shouldLog() {
55         try {
56             return Settings.Global.getInt(
57                     sTestApis.context().instrumentedContext().getContentResolver(),
58                     "nene_log") == 1;
59         } catch (Settings.SettingNotFoundException e) {
60             return false;
61         }
62     }
63 
ShellCommandUtils()64     private ShellCommandUtils() { }
65 
66     /**
67      * Execute an adb shell command.
68      *
69      * <p>When running on S and above, any failures in executing the command will result in an
70      * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException}
71      * will be thrown when the command returns no output (indicating that there is an error on
72      * stderr which cannot be read by this method) but some failures will return seemingly correctly
73      * but with an error in the returned string.
74      *
75      * <p>Callers should be careful to check the command's output is valid.
76      */
executeCommand(String command)77     static String executeCommand(String command) throws AdbException {
78         return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null);
79     }
80 
executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)81     static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)
82             throws AdbException {
83         logCommand(command, allowEmptyOutput, stdInBytes);
84 
85         if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
86             if (stdInBytes != null && stdInBytes.length > 0) {
87                 throw new IllegalStateException("Cannot write to stdIn prior to S");
88             }
89             return executeCommandPreS(command, allowEmptyOutput);
90         }
91 
92         // TODO(scottjonathan): Add argument to force errors to stderr
93         try {
94 
95             ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
96             ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
97             ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
98             ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
99 
100             writeStdInAndClose(fdIn, stdInBytes);
101 
102             String out = new String(readStreamAndClose(fdOut));
103             String err = new String(readStreamAndClose(fdErr));
104 
105             if (!err.isEmpty()) {
106                 throw new AdbException("Error executing command", command, out, err);
107             }
108 
109             if (SHOULD_LOG) {
110                 Log.d(LOG_TAG, "Command result: " + out);
111             }
112 
113             return out;
114         } catch (IOException e) {
115             throw new AdbException("Error executing command", command, e);
116         }
117     }
118 
executeCommandForBytes(String command)119     static byte[] executeCommandForBytes(String command) throws AdbException {
120         return executeCommandForBytes(command, /* stdInBytes= */ null);
121     }
122 
executeCommandForBytes(String command, byte[] stdInBytes)123     static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException {
124         logCommand(command, /* allowEmptyOutput= */ false, stdInBytes);
125 
126         if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
127             if (stdInBytes != null && stdInBytes.length > 0) {
128                 throw new IllegalStateException("Cannot write to stdIn prior to S");
129             }
130 
131             return executeCommandForBytesPreS(command);
132         }
133 
134         // TODO(scottjonathan): Add argument to force errors to stderr
135         try {
136 
137             ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
138             ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
139             ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
140             ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
141 
142             writeStdInAndClose(fdIn, stdInBytes);
143 
144             byte[] out = readStreamAndClose(fdOut);
145             String err = new String(readStreamAndClose(fdErr));
146 
147             if (!err.isEmpty()) {
148                 throw new AdbException("Error executing command", command, err);
149             }
150 
151             return out;
152         } catch (IOException e) {
153             throw new AdbException("Error executing command", command, e);
154         }
155     }
156 
logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)157     private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) {
158         if (!SHOULD_LOG) {
159             return;
160         }
161 
162         StringBuilder logBuilder = new StringBuilder("Executing shell command ");
163         logBuilder.append(command);
164         if (allowEmptyOutput) {
165             logBuilder.append(" (allow empty output)");
166         }
167         if (stdInBytes != null) {
168             logBuilder.append(" (writing to stdIn)");
169         }
170         Log.d(LOG_TAG, logBuilder.toString());
171     }
172 
173     /**
174      * Execute an adb shell command and check that the output meets a given criteria.
175      *
176      * <p>On S and above, any output printed to standard error will result in an exception and the
177      * {@code outputSuccessChecker} not being called. Empty output will still be processed.
178      *
179      * <p>Prior to S, if there is no output on standard out, regardless of if there is output on
180      * standard error, {@code outputSuccessChecker} will not be called.
181      *
182      * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
183      * command executed successfully.
184      */
executeCommandAndValidateOutput( String command, Function<String, Boolean> outputSuccessChecker)185     static String executeCommandAndValidateOutput(
186             String command, Function<String, Boolean> outputSuccessChecker) throws AdbException {
187         return executeCommandAndValidateOutput(command,
188                 /* allowEmptyOutput= */ false,
189                 /* stdInBytes= */ null,
190                 outputSuccessChecker);
191     }
192 
executeCommandAndValidateOutput( String command, boolean allowEmptyOutput, byte[] stdInBytes, Function<String, Boolean> outputSuccessChecker)193     static String executeCommandAndValidateOutput(
194             String command,
195             boolean allowEmptyOutput,
196             byte[] stdInBytes,
197             Function<String, Boolean> outputSuccessChecker) throws AdbException {
198         String output = executeCommand(command, allowEmptyOutput, stdInBytes);
199         if (!outputSuccessChecker.apply(output)) {
200             throw new AdbException("Command did not meet success criteria", command, output);
201         }
202         return output;
203     }
204 
205     /**
206      * Return {@code true} if {@code output} starts with "success", case insensitive.
207      */
startsWithSuccess(String output)208     public static boolean startsWithSuccess(String output) {
209         return output.toUpperCase().startsWith("SUCCESS");
210     }
211 
212     /**
213      * Return {@code true} if {@code output} does not start with "error", case insensitive.
214      */
doesNotStartWithError(String output)215     public static boolean doesNotStartWithError(String output) {
216         return !output.toUpperCase().startsWith("ERROR");
217     }
218 
executeCommandPreS( String command, boolean allowEmptyOutput)219     private static String executeCommandPreS(
220             String command, boolean allowEmptyOutput) throws AdbException {
221         ParcelFileDescriptor fdOut = uiAutomation().executeShellCommand(command);
222 
223         try {
224             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
225                 String out = new String(FileUtils.readInputStreamFully(fis));
226 
227                 if (!allowEmptyOutput && out.isEmpty()) {
228                     throw new AdbException(
229                             "No output from command. There's likely an error on stderr",
230                             command, out);
231                 }
232 
233                 if (SHOULD_LOG) {
234                     Log.d(LOG_TAG, "Command result: " + out);
235                 }
236 
237                 return out;
238             }
239         } catch (IOException e) {
240             throw new AdbException(
241                     "Error reading command output", command, e);
242         }
243     }
244 
executeCommandForBytesPreS(String command)245     private static byte[] executeCommandForBytesPreS(String command) throws AdbException {
246         ParcelFileDescriptor fdOut = uiAutomation().executeShellCommand(command);
247 
248         try {
249             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
250                 return FileUtils.readInputStreamFully(fis);
251             }
252         } catch (IOException e) {
253             throw new AdbException(
254                     "Error reading command output", command, e);
255         }
256     }
257 
writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)258     private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)
259             throws IOException {
260         if (stdInBytes != null) {
261             try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) {
262                 fos.write(stdInBytes);
263             }
264         } else {
265             fdIn.close();
266         }
267     }
268 
readStreamAndClose(ParcelFileDescriptor fd)269     private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException {
270         try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
271             return FileUtils.readInputStreamFully(fis);
272         }
273     }
274 
275     /**
276      * Get a {@link UiAutomation}.
277      */
uiAutomation()278     public static UiAutomation uiAutomation() {
279         return InstrumentationRegistry.getInstrumentation().getUiAutomation();
280     }
281 }
282