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