Home | History | Annotate | Download | only in android
      1 /*
      2  * Copyright (C) 2010 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 vogar.android;
     18 
     19 import com.google.common.annotations.VisibleForTesting;
     20 import com.google.common.collect.ImmutableList;
     21 import java.io.File;
     22 import java.io.FileNotFoundException;
     23 import java.util.List;
     24 import java.util.concurrent.TimeoutException;
     25 import java.util.regex.Matcher;
     26 import java.util.regex.Pattern;
     27 import vogar.Log;
     28 import vogar.Md5Cache;
     29 import vogar.Target;
     30 import vogar.commands.Command;
     31 
     32 public final class AdbTarget extends Target {
     33 
     34     private static final ImmutableList<String> TARGET_PROCESS_PREFIX =
     35             ImmutableList.of("adb", "shell");
     36 
     37     private final Log log;
     38 
     39     private final DeviceFilesystem deviceFilesystem;
     40 
     41     private final Md5Cache pushCache;
     42 
     43    @VisibleForTesting
     44    public AdbTarget(Log log, DeviceFilesystem deviceFilesystem, DeviceFileCache deviceFileCache) {
     45        this.log = log;
     46        this.deviceFilesystem = deviceFilesystem;
     47        this.pushCache =
     48                deviceFileCache == null ? null : new Md5Cache(log, "pushed", deviceFileCache);
     49     }
     50 
     51     public static File defaultDeviceDir() {
     52         return new File("/data/local/tmp/vogar");
     53     }
     54 
     55     @Override protected ImmutableList<String> targetProcessPrefix() {
     56         return TARGET_PROCESS_PREFIX;
     57     }
     58 
     59     @Override public void await(File directory) {
     60         waitForDevice();
     61         ensureDirectory(directory);
     62         remount();
     63     }
     64 
     65     private void waitForDevice() {
     66         new Command.Builder(log)
     67             .args("adb", "wait-for-device")
     68             .permitNonZeroExitStatus(true)
     69             .execute();
     70     }
     71 
     72     /**
     73      * Make sure the directory exists.
     74      */
     75     private void ensureDirectory(File directory) {
     76         String pathArgument = directory.getPath() + "/";
     77         if (pathArgument.equals("/sdcard/")) {
     78             // /sdcard is a mount point. If it exists but is empty we do
     79             // not want to use it. So we wait until it is not empty.
     80             waitForNonEmptyDirectory(pathArgument, 5 * 60);
     81         } else {
     82             Command command = new Command.Builder(log)
     83                 .args("adb", "shell", "ls", pathArgument)
     84                 .permitNonZeroExitStatus(true)
     85                 .build();
     86             List<String> output = command.execute();
     87             // TODO: We should avoid checking for the error message, and instead have
     88             // the Command class understand a non-zero exit code from an adb shell command.
     89             if (!output.isEmpty()
     90                 && output.get(0).equals(pathArgument + ": No such file or directory")) {
     91                 throw new RuntimeException("'" + pathArgument + "' does not exist on device");
     92             }
     93             // Otherwise the directory exists.
     94         }
     95     }
     96 
     97     private void remount() {
     98         new Command(log, "adb", "remount").execute();
     99     }
    100 
    101     private void waitForNonEmptyDirectory(String pathArgument, int timeoutSeconds) {
    102         final int millisPerSecond = 1000;
    103         final long start = System.currentTimeMillis();
    104         final long deadline = start + (millisPerSecond * timeoutSeconds);
    105 
    106         while (true) {
    107             final int remainingSeconds =
    108                     (int) ((deadline - System.currentTimeMillis()) / millisPerSecond);
    109             Command command = new Command.Builder(log)
    110                     .args("adb", "shell", "ls", pathArgument)
    111                     .permitNonZeroExitStatus(true)
    112                     .build();
    113             List<String> output;
    114             try {
    115                 output = command.executeWithTimeout(remainingSeconds);
    116             } catch (TimeoutException e) {
    117                 throw new RuntimeException("Timed out after " + timeoutSeconds
    118                         + " seconds waiting for " + pathArgument, e);
    119             }
    120             try {
    121                 Thread.sleep(millisPerSecond);
    122             } catch (InterruptedException e) {
    123                 throw new RuntimeException(e);
    124             }
    125 
    126             // We just want any output.
    127             if (!output.isEmpty()) {
    128                 return;
    129             }
    130 
    131             log.warn("Waiting on " + pathArgument + " to be mounted ");
    132         }
    133     }
    134 
    135     @Override public List<File> ls(File directory) throws FileNotFoundException {
    136         return deviceFilesystem.ls(directory);
    137     }
    138 
    139     @Override public String getDeviceUserName() {
    140         // The default environment doesn't include $USER, so dalvikvm doesn't set "user.name".
    141         // DeviceRuntime uses this to set "user.name" manually with -D.
    142         String line = new Command(log, "adb", "shell", "id").execute().get(0);
    143         // TODO: use 'id -un' when we don't need to support anything older than M
    144         Matcher m = Pattern.compile("^uid=\\d+\\((\\S+)\\) gid=\\d+\\(\\S+\\).*").matcher(line);
    145         return m.matches() ? m.group(1) : "root";
    146     }
    147 
    148     @Override public void rm(File file) {
    149         new Command.Builder(log).args("adb", "shell", "rm", "-r", file.getPath())
    150                 // Note: When all supported versions of Android correctly return the exit code
    151                 // from adb we can rely on the exit code to detect failure. Until then: no.
    152                 .permitNonZeroExitStatus(true)
    153                 .execute();
    154     }
    155 
    156     @Override public void mkdirs(File file) {
    157         deviceFilesystem.mkdirs(file);
    158     }
    159 
    160     @Override public void forwardTcp(int port) {
    161         new Command(log, "adb", "forward", "tcp:" + port, "tcp:" + port).execute();
    162     }
    163 
    164     @Override public void push(File local, File remote) {
    165         Command fallback = new Command(log, "adb", "push", local.getPath(), remote.getPath());
    166         deviceFilesystem.mkdirs(remote.getParentFile());
    167 
    168         // don't yet cache directories (only used by jtreg tests)
    169         if (pushCache != null && local.isFile()) {
    170             String key = pushCache.makeKey(local);
    171             boolean cacheHit = pushCache.getFromCache(remote, key);
    172             if (cacheHit) {
    173                 log.verbose("device cache hit for " + local);
    174                 return;
    175             }
    176             fallback.execute();
    177             pushCache.insert(key, remote);
    178         } else {
    179             fallback.execute();
    180         }
    181     }
    182 
    183     @Override public void pull(File remote, File local) {
    184         new Command(log, "adb", "pull", remote.getPath(), local.getPath()).execute();
    185     }
    186 }
    187