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