| /* |
| * 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(); |
| } |
| } |