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 }