blob: 7818bcc72eb7b25c8d2a082662950bd50b34acfb [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bedstead.nene.utils;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.users.UserReference;
import java.util.function.Function;
/**
* A tool for progressively building and then executing a shell command.
*/
public final class ShellCommand {
// 10 seconds
private static final int MAX_WAIT_UNTIL_ATTEMPTS = 100;
private static final long WAIT_UNTIL_DELAY_MILLIS = 100;
/**
* Begin building a new {@link ShellCommand}.
*/
@CheckResult
public static Builder builder(String command) {
if (command == null) {
throw new NullPointerException();
}
return new Builder(command);
}
/**
* Create a builder and if {@code userReference} is not {@code null}, add "--user <userId>".
*/
@CheckResult
public static Builder builderForUser(@Nullable UserReference userReference, String command) {
Builder builder = builder(command);
if (userReference != null) {
builder.addOption("--user", userReference.id());
}
return builder;
}
public static final class Builder {
private String mLinuxUser;
private final StringBuilder commandBuilder;
@Nullable
private byte[] mStdInBytes = null;
@Nullable
private boolean mAllowEmptyOutput = false;
@Nullable
private Function<String, Boolean> mOutputSuccessChecker = null;
private Builder(String command) {
commandBuilder = new StringBuilder(command);
}
/**
* Add an option to the command.
*
* <p>e.g. --user 10
*/
@CheckResult
public Builder addOption(String key, Object value) {
// TODO: Deal with spaces/etc.
commandBuilder.append(" ").append(key).append(" ").append(value);
return this;
}
/**
* Add an operand to the command.
*/
@CheckResult
public Builder addOperand(Object value) {
// TODO: Deal with spaces/etc.
commandBuilder.append(" ").append(value);
return this;
}
/**
* If {@code false} an error will be thrown if the command has no output.
*
* <p>Defaults to {@code false}
*/
@CheckResult
public Builder allowEmptyOutput(boolean allowEmptyOutput) {
mAllowEmptyOutput = allowEmptyOutput;
return this;
}
/**
* Write the given {@code stdIn} to standard in.
*/
@CheckResult
public Builder writeToStdIn(byte[] stdIn) {
mStdInBytes = stdIn;
return this;
}
/**
* Validate the output when executing.
*
* <p>{@code outputSuccessChecker} should return {@code true} if the output is valid.
*/
@CheckResult
public Builder validate(Function<String, Boolean> outputSuccessChecker) {
mOutputSuccessChecker = outputSuccessChecker;
return this;
}
/**
* Build the full command including all options and operands.
*/
public String build() {
if (mLinuxUser != null) {
return "su " + mLinuxUser + " " + commandBuilder.toString();
}
return commandBuilder.toString();
}
/**
* See {@link #execute()} except that any {@link AdbException} is wrapped in a
* {@link NeneException} with the message {@code errorMessage}.
*/
public String executeOrThrowNeneException(String errorMessage) throws NeneException {
try {
return execute();
} catch (AdbException e) {
throw new NeneException(errorMessage, e);
}
}
/** See {@link ShellCommandUtils#executeCommand(java.lang.String)}. */
public String execute() throws AdbException {
if (mOutputSuccessChecker != null) {
return ShellCommandUtils.executeCommandAndValidateOutput(
build(),
/* allowEmptyOutput= */ mAllowEmptyOutput,
mStdInBytes,
mOutputSuccessChecker);
}
return ShellCommandUtils.executeCommand(
build(),
/* allowEmptyOutput= */ mAllowEmptyOutput,
mStdInBytes);
}
/**
* See {@link #execute} and then extract information from the output using
* {@code outputParser}.
*
* <p>If any {@link Exception} is thrown by {@code outputParser}, and {@link AdbException}
* will be thrown.
*/
public <E> E executeAndParseOutput(Function<String, E> outputParser) throws AdbException {
String output = execute();
try {
return outputParser.apply(output);
} catch (RuntimeException e) {
throw new AdbException(
"Could not parse output", commandBuilder.toString(), output, e);
}
}
/**
* Execute the command and check that the output meets a given criteria. Run the
* command repeatedly until the output meets the criteria.
*
* <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
* command executed successfully.
*/
public String executeUntilValid() throws InterruptedException, AdbException {
int attempts = 0;
while (attempts++ < MAX_WAIT_UNTIL_ATTEMPTS) {
try {
return execute();
} catch (AdbException e) {
// ignore, will retry
Thread.sleep(WAIT_UNTIL_DELAY_MILLIS);
}
}
return execute();
}
public BytesBuilder forBytes() {
if (mOutputSuccessChecker != null) {
throw new IllegalStateException("Cannot call .forBytes after .validate");
}
return new BytesBuilder(this);
}
@Override
public String toString() {
return "ShellCommand$Builder{cmd=" + build() + "}";
}
}
public static final class BytesBuilder {
private final Builder mBuilder;
private BytesBuilder(Builder builder) {
mBuilder = builder;
}
/** See {@link ShellCommandUtils#executeCommandForBytes(java.lang.String)}. */
public byte[] execute() throws AdbException {
return ShellCommandUtils.executeCommandForBytes(
mBuilder.build(),
mBuilder.mStdInBytes);
}
}
}