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