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 }