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 androidx.annotation.CheckResult;
20 import androidx.annotation.Nullable;
21 
22 import com.android.bedstead.nene.exceptions.AdbException;
23 import com.android.bedstead.nene.exceptions.NeneException;
24 import com.android.bedstead.nene.users.UserReference;
25 
26 import com.google.errorprone.annotations.CanIgnoreReturnValue;
27 
28 import java.time.Duration;
29 import java.util.concurrent.CountDownLatch;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.atomic.AtomicReference;
32 import java.util.function.Function;
33 
34 /**
35  * A tool for progressively building and then executing a shell command.
36  */
37 public final class ShellCommand {
38 
39     // 10 seconds
40     private static final int MAX_WAIT_UNTIL_ATTEMPTS = 100;
41     private static final long WAIT_UNTIL_DELAY_MILLIS = 100;
42 
43     /**
44      * Begin building a new {@link ShellCommand}.
45      */
46     @CheckResult
builder(String command)47     public static Builder builder(String command) {
48         if (command == null) {
49             throw new NullPointerException();
50         }
51         return new Builder(command);
52     }
53 
54     /**
55      * Create a builder and if {@code userReference} is not {@code null}, add "--user <userId>".
56      */
57     @CheckResult
builderForUser(@ullable UserReference userReference, String command)58     public static Builder builderForUser(@Nullable UserReference userReference, String command) {
59         Builder builder = builder(command);
60         if (userReference == null) {
61             return builder;
62         }
63 
64         return builder.addOption("--user", userReference.id());
65     }
66 
67     public static final class Builder {
68         private final StringBuilder commandBuilder;
69         @Nullable
70         private byte[] mStdInBytes = null;
71         @Nullable
72         private Duration mTimeout = null;
73         private boolean mAllowEmptyOutput = false;
74         @Nullable
75         private Function<String, Boolean> mOutputSuccessChecker = null;
76         private boolean mShouldRunAsRootWithSuperUser = false;
77 
Builder(String command)78         private Builder(String command) {
79             commandBuilder = new StringBuilder(command);
80         }
81 
82         /**
83          * Run command as root by adding {@code su root} as prefix if needed.
84          * <br><br>
85          * Note: If shell has access to root but {@code su} is not available the {@code su root}
86          * prefix will not be added as shell is probably running as root. This can be checked
87          * using {@code ShellCommandUtils.isRunningAsRoot}.
88          */
asRoot(boolean shouldRunAsRoot)89         public Builder asRoot(boolean shouldRunAsRoot) {
90             mShouldRunAsRootWithSuperUser = shouldRunAsRoot &&
91                     !ShellCommandUtils.isRunningAsRoot() &&
92                     ShellCommandUtils.isSuperUserAvailable();
93             return this;
94         }
95 
96         /**
97          * Add an option to the command.
98          *
99          * <p>e.g. --user 10
100          */
101         @CanIgnoreReturnValue
102         @CheckResult
addOption(String key, Object value)103         public Builder addOption(String key, Object value) {
104             // TODO: Deal with spaces/etc.
105             commandBuilder.append(" ").append(key).append(" ").append(value);
106             return this;
107         }
108 
109         /**
110          * Add an operand to the command.
111          */
112         @CanIgnoreReturnValue
113         @CheckResult
addOperand(Object value)114         public Builder addOperand(Object value) {
115             // TODO: Deal with spaces/etc.
116             commandBuilder.append(" ").append(value);
117             return this;
118         }
119 
120         /**
121          * Add a timeout to the execution of the command.
122          */
123         @CanIgnoreReturnValue
withTimeout(Duration timeout)124         public Builder withTimeout(Duration timeout) {
125             mTimeout = timeout;
126             return this;
127         }
128 
129         /**
130          * If {@code false} an error will be thrown if the command has no output.
131          *
132          * <p>Defaults to {@code false}
133          */
134         @CheckResult
allowEmptyOutput(boolean allowEmptyOutput)135         public Builder allowEmptyOutput(boolean allowEmptyOutput) {
136             mAllowEmptyOutput = allowEmptyOutput;
137             return this;
138         }
139 
140         /**
141          * Write the given {@code stdIn} to standard in.
142          */
143         @CheckResult
writeToStdIn(byte[] stdIn)144         public Builder writeToStdIn(byte[] stdIn) {
145             mStdInBytes = stdIn;
146             return this;
147         }
148 
149         /**
150          * Validate the output when executing.
151          *
152          * <p>{@code outputSuccessChecker} should return {@code true} if the output is valid.
153          */
154         @CheckResult
validate(Function<String, Boolean> outputSuccessChecker)155         public Builder validate(Function<String, Boolean> outputSuccessChecker) {
156             mOutputSuccessChecker = outputSuccessChecker;
157             return this;
158         }
159 
160         /**
161          * Build the full command including all options and operands.
162          */
build()163         public String build() {
164             if (mShouldRunAsRootWithSuperUser) {
165                 return commandBuilder.insert(0, "su root ").toString();
166             }
167 
168             return commandBuilder.toString();
169         }
170 
171         /**
172          * See {@link #execute()} except that any {@link AdbException} is wrapped in a
173          * {@link NeneException} with the message {@code errorMessage}.
174          */
175         @CanIgnoreReturnValue
executeOrThrowNeneException(String errorMessage)176         public String executeOrThrowNeneException(String errorMessage) throws NeneException {
177             try {
178                 return execute();
179             } catch (AdbException e) {
180                 throw new NeneException(errorMessage, e);
181             }
182         }
183 
184         /** See {@link ShellCommandUtils#executeCommand(java.lang.String)}. */
185         @CanIgnoreReturnValue
execute()186         public String execute() throws AdbException {
187             if (mTimeout == null) {
188                 return executeSync();
189             }
190 
191             AtomicReference<AdbException> adbException = new AtomicReference<>(null);
192             AtomicReference<String> result = new AtomicReference<>(null);
193 
194             CountDownLatch latch = new CountDownLatch(1);
195 
196             Thread thread = new Thread(() -> {
197                 try {
198                     result.set(executeSync());
199                 } catch (AdbException e) {
200                     adbException.set(e);
201                 } finally {
202                     latch.countDown();
203                 }
204             });
205             thread.start();
206 
207             try {
208                 if (!latch.await(mTimeout.toMillis(), TimeUnit.MILLISECONDS)) {
209                     throw new AdbException("Command could not run in " + mTimeout, build(), "");
210                 }
211             } catch (InterruptedException e) {
212                 throw new AdbException("Interrupted while executing command", build(), "", e);
213             }
214 
215             if (adbException.get() != null) {
216                 throw adbException.get();
217             }
218 
219             return result.get();
220         }
221 
executeSync()222         private String executeSync() throws AdbException {
223             if (mOutputSuccessChecker != null) {
224                 return ShellCommandUtils.executeCommandAndValidateOutput(
225                         build(),
226                         /* allowEmptyOutput= */ mAllowEmptyOutput,
227                         mStdInBytes,
228                         mOutputSuccessChecker);
229             }
230 
231             return ShellCommandUtils.executeCommand(
232                     build(),
233                     /* allowEmptyOutput= */ mAllowEmptyOutput,
234                     mStdInBytes);
235         }
236 
237         /**
238          * See {@link #execute} and then extract information from the output using
239          * {@code outputParser}.
240          *
241          * <p>If any {@link Exception} is thrown by {@code outputParser}, and {@link AdbException}
242          * will be thrown.
243          */
executeAndParseOutput(Function<String, E> outputParser)244         public <E> E executeAndParseOutput(Function<String, E> outputParser) throws AdbException {
245             String output = execute();
246 
247             try {
248                 return outputParser.apply(output);
249             } catch (RuntimeException e) {
250                 throw new AdbException(
251                         "Could not parse output", commandBuilder.toString(), output, e);
252             }
253         }
254 
255         /**
256          * Execute the command and check that the output meets a given criteria. Run the
257          * command repeatedly until the output meets the criteria.
258          *
259          * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
260          * command executed successfully.
261          */
executeUntilValid()262         public String executeUntilValid() throws InterruptedException, AdbException {
263             int maxWaitUntilAttempts = MAX_WAIT_UNTIL_ATTEMPTS;
264             long waitUntilDelayMillis = WAIT_UNTIL_DELAY_MILLIS;
265             return executeUntilValid(maxWaitUntilAttempts, waitUntilDelayMillis);
266         }
267 
268         /**
269          * Execute the command and check that the output meets a given criteria. Run the
270          * command repeatedly until the output meets the criteria.
271          *
272          * @param maxWaitUntilAttempts maximum number of attempts
273          * @param waitUntilDelayMillis minimum interval between calls in milliseconds
274          * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
275          * command executed successfully.
276          */
executeUntilValid(int maxWaitUntilAttempts, long waitUntilDelayMillis)277         public String executeUntilValid(int maxWaitUntilAttempts, long waitUntilDelayMillis) throws
278                 InterruptedException, AdbException {
279             int attempts = 0;
280             while (attempts++ < maxWaitUntilAttempts) {
281                 try {
282                     return execute();
283                 } catch (AdbException e) {
284                     // ignore, will retry
285                     Thread.sleep(waitUntilDelayMillis);
286                 }
287             }
288             return execute();
289         }
290 
forBytes()291         public BytesBuilder forBytes() {
292             if (mOutputSuccessChecker != null) {
293                 throw new IllegalStateException("Cannot call .forBytes after .validate");
294             }
295 
296             return new BytesBuilder(this);
297         }
298 
299         @Override
toString()300         public String toString() {
301             return "ShellCommand$Builder{cmd=" + build() + "}";
302         }
303     }
304 
305     public static final class BytesBuilder {
306 
307         private final Builder mBuilder;
308 
BytesBuilder(Builder builder)309         private BytesBuilder(Builder builder) {
310             mBuilder = builder;
311         }
312 
313         /** See {@link ShellCommandUtils#executeCommandForBytes(java.lang.String)}. */
execute()314         public byte[] execute() throws AdbException {
315             return ShellCommandUtils.executeCommandForBytes(
316                     mBuilder.build(),
317                     mBuilder.mStdInBytes);
318         }
319     }
320 }