blob: 44bf8686994db5fc342800e67d767367a5fc5859 [file] [log] [blame]
/*
* 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.android;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import vogar.Log;
import vogar.Md5Cache;
import vogar.Target;
import vogar.commands.Command;
public class AdbTarget extends Target {
private static final ImmutableList<String> TARGET_PROCESS_PREFIX =
ImmutableList.of("adb", "shell");
protected final Log log;
private final DeviceFilesystem deviceFilesystem;
private final Md5Cache pushCache;
@VisibleForTesting
public AdbTarget(Log log, DeviceFilesystem deviceFilesystem, DeviceFileCache deviceFileCache) {
this.log = log;
this.deviceFilesystem = deviceFilesystem;
this.pushCache =
deviceFileCache == null ? null : new Md5Cache(log, "pushed", deviceFileCache);
}
public static File defaultDeviceDir() {
return new File("/data/local/tmp/vogar");
}
@Override protected ImmutableList<String> targetProcessPrefix() {
return TARGET_PROCESS_PREFIX;
}
@Override public void await(File directory) {
waitForDevice();
ensureDirectory(directory);
remount();
}
private void waitForDevice() {
new Command.Builder(log)
.args("adb", "wait-for-device")
.permitNonZeroExitStatus(true)
.execute();
}
/**
* Make sure the directory exists.
*/
private void ensureDirectory(File directory) {
String pathArgument = directory.getPath() + "/";
if (pathArgument.equals("/sdcard/")) {
// /sdcard is a mount point. If it exists but is empty we do
// not want to use it. So we wait until it is not empty.
waitForNonEmptyDirectory(pathArgument, 5 * 60);
} else {
Command command = new Command.Builder(log)
.args("adb", "shell", "ls", pathArgument)
.permitNonZeroExitStatus(true)
.build();
List<String> output = command.execute();
// TODO: We should avoid checking for the error message, and instead have
// the Command class understand a non-zero exit code from an adb shell command.
if (!output.isEmpty()
&& output.get(0).equals(pathArgument + ": No such file or directory")) {
throw new RuntimeException("'" + pathArgument + "' does not exist on device");
}
// Otherwise the directory exists.
}
}
private void remount() {
new Command(log, "adb", "remount").execute();
}
private void waitForNonEmptyDirectory(String pathArgument, int timeoutSeconds) {
final int millisPerSecond = 1000;
final long start = System.currentTimeMillis();
final long deadline = start + (millisPerSecond * timeoutSeconds);
while (true) {
final int remainingSeconds =
(int) ((deadline - System.currentTimeMillis()) / millisPerSecond);
Command command = new Command.Builder(log)
.args("adb", "shell", "ls", pathArgument)
.permitNonZeroExitStatus(true)
.build();
List<String> output;
try {
output = command.executeWithTimeout(remainingSeconds);
} catch (TimeoutException e) {
throw new RuntimeException("Timed out after " + timeoutSeconds
+ " seconds waiting for " + pathArgument, e);
}
try {
Thread.sleep(millisPerSecond);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// We just want any output.
if (!output.isEmpty()) {
return;
}
log.warn("Waiting on " + pathArgument + " to be mounted ");
}
}
@Override public List<File> ls(File directory) throws FileNotFoundException {
return deviceFilesystem.ls(directory);
}
@Override public String getDeviceUserName() {
// The default environment doesn't include $USER, so dalvikvm doesn't set "user.name".
// DeviceRuntime uses this to set "user.name" manually with -D.
String line = new Command(log, "adb", "shell", "id").execute().get(0);
// TODO: use 'id -un' when we don't need to support anything older than M
Matcher m = Pattern.compile("^uid=\\d+\\((\\S+)\\) gid=\\d+\\(\\S+\\).*").matcher(line);
return m.matches() ? m.group(1) : "root";
}
@Override public void rm(File file) {
new Command.Builder(log).args("adb", "shell", "rm", "-r", file.getPath())
// Note: When all supported versions of Android correctly return the exit code
// from adb we can rely on the exit code to detect failure. Until then: no.
.permitNonZeroExitStatus(true)
.execute();
}
@Override public void mkdirs(File file) {
deviceFilesystem.mkdirs(file);
}
@Override public void forwardTcp(int port) {
new Command(log, "adb", "forward", "tcp:" + port, "tcp:" + port).execute();
}
@Override public void push(File local, File remote) {
Command fallback = new Command(log, "adb", "push", local.getPath(), remote.getPath());
deviceFilesystem.mkdirs(remote.getParentFile());
// don't yet cache directories (only used by jtreg tests)
if (pushCache != null && local.isFile()) {
String key = pushCache.makeKey(local);
boolean cacheHit = pushCache.getFromCache(remote, key);
if (cacheHit) {
log.verbose("device cache hit for " + local);
return;
}
fallback.execute();
pushCache.insert(key, remote);
} else {
fallback.execute();
}
}
@Override public void pull(File remote, File local) {
new Command(log, "adb", "pull", remote.getPath(), local.getPath()).execute();
}
}