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