/*
* Copyright (C) 2010 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 vogar;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import vogar.tasks.Task;
/**
* A target runtime environment such as a remote device or the local host
*/
public abstract class Target {
protected abstract ImmutableList<String> targetProcessPrefix();
public abstract String getDeviceUserName();
public abstract List<File> ls(File directory) throws FileNotFoundException;
public abstract void await(File nonEmptyDirectory);
public abstract void rm(File file);
public abstract void mkdirs(File file);
public abstract void forwardTcp(int port);
public abstract void push(File local, File remote);
public abstract void pull(File remote, File local);
public final Task pushTask(final File local, final File remote) {
return new Task("push " + remote) {
@Override protected Result execute() throws Exception {
push(local, remote);
return Result.SUCCESS;
}
};
}
public final Task rmTask(final File remote) {
return new Task("rm " + remote) {
@Override protected Result execute() throws Exception {
rm(remote);
return Result.SUCCESS;
}
};
}
/**
* Create a {@link ScriptBuilder} appropriate for this target.
*/
public ScriptBuilder newScriptBuilder() {
return new ScriptBuilder(targetProcessPrefix());
}
/**
* Responsible for constructing a one line script appropriate for a specific target.
*/
public static class ScriptBuilder {
private static final Joiner SCRIPT_JOINER = Joiner.on(" ");
/**
* Escape any special shell characters so that the target shell treats them as literal
* characters.
*
* <p>e.g. an escaped space will not be treated as a token separator, an escaped dollar will
* not cause shell substitution.
*/
@VisibleForTesting
static String escape(String token) {
int length = token.length();
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; ++i) {
char c = token.charAt(i);
if (Character.isWhitespace(c)
|| c == '\'' || c == '\"'
|| c == '|' || c == '&'
|| c == '<' || c == '>'
|| c == '$' || c == '!'
|| c == '(' || c == ')') {
builder.append('\\');
}
builder.append(c);
}
return builder.toString();
}
/**
* The prefix to insert before the script to produce a command line that will execute the
* script within a shell.
*/
private final ImmutableList<String> commandLinePrefix;
/**
* The list of tokens making up the script, they were escaped where necessary before they
* were added to the list.
*/
private final List<String> escapedTokens;
private ScriptBuilder(ImmutableList<String> commandLinePrefix) {
this.commandLinePrefix = commandLinePrefix;
escapedTokens = new ArrayList<>();
}
/**
* Set the working directory in the target shell before running the command.
*/
public ScriptBuilder workingDirectory(File workingDirectory) {
escapedTokens.add("cd");
escapedTokens.add(escape(workingDirectory.getPath()));
escapedTokens.add("&&");
return this;
}
/**
* Set inline environment variables on the target shell that will affect the command.
*/
public void env(Map<String, String> env) {
for (Map.Entry<String, String> entry : env.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
escapedTokens.add(name + "=" + escape(value));
}
}
/**
* Add tokens to the script.
*
* <p>Each token is escaped before adding it to the list. This method is suitable for adding
* the command line name and arguments.
*/
public ScriptBuilder tokens(List<String> tokens) {
for (String token : tokens) {
escapedTokens.add(escape(token));
}
return this;
}
/**
* Add tokens to the script.
*
* <p>Each token is escaped before adding it to the list. This method is suitable for adding
* the command line name and arguments.
*/
public ScriptBuilder tokens(String... tokens) {
for (String token : tokens) {
escapedTokens.add(escape(token));
}
return this;
}
/**
* Construct a command line to execute the script in the target shell.
*/
public List<String> commandLine() {
// Group the tokens into a single String argument. This is necessary for running in
// a local shell where the first argument after the -c option is the script and the
// remainder are treated as arguments to the script. This has no effect on either
// adb or ssh shells as they both concatenate all their arguments into one single
// string before parsing.
String grouped = SCRIPT_JOINER.join(escapedTokens);
return new ImmutableList.Builder<String>()
.addAll(commandLinePrefix)
.add(grouped)
.build();
}
}
}