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 }