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