| // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE file. | 
 | package com.android.tools.r8; | 
 |  | 
 | import com.android.ddmlib.AdbCommandRejectedException; | 
 | import com.android.ddmlib.AndroidDebugBridge; | 
 | import com.android.ddmlib.FileListingService; | 
 | import com.android.ddmlib.IDevice; | 
 | import com.android.ddmlib.IShellOutputReceiver; | 
 | import com.android.ddmlib.ShellCommandUnresponsiveException; | 
 | import com.android.ddmlib.SyncException; | 
 | import com.android.ddmlib.TimeoutException; | 
 | import com.android.tools.r8.utils.StringUtils; | 
 | import com.google.common.base.Joiner; | 
 | import java.io.BufferedReader; | 
 | import java.io.ByteArrayOutputStream; | 
 | import java.io.File; | 
 | import java.io.FileNotFoundException; | 
 | import java.io.FileReader; | 
 | import java.io.IOException; | 
 | import java.io.OutputStream; | 
 | import java.io.PrintStream; | 
 | import java.nio.file.Path; | 
 | import java.util.ArrayList; | 
 | import java.util.Arrays; | 
 | import java.util.Collections; | 
 | import java.util.List; | 
 | import java.util.Map; | 
 | import java.util.stream.Collectors; | 
 |  | 
 | /** | 
 |  * This class is used to run tests on devices (or emulators) | 
 |  * using ddmlib. | 
 |  */ | 
 | public class DeviceRunner { | 
 |  | 
 |   private static final boolean VERBOSE = false; | 
 |   private static final long ADB_CONNECTION_TIMEOUT = 5000; | 
 |   private static final long ADB_WAIT_STEP = ADB_CONNECTION_TIMEOUT / 10; | 
 |   private static final String TEST_SCRIPT_NAME = "test-exit-status.sh"; | 
 |   private static final File TEST_SCRIPT_FILE = | 
 |       new File(System.getProperty("user.dir"), "scripts/" + TEST_SCRIPT_NAME); | 
 |   private static final char PATH_SEPARATOR_CHAR = ':'; | 
 |   private static final String RUNTIME_NAME = "ART"; | 
 |  | 
 |   private List<String> vmOptions = Collections.emptyList(); | 
 |   private Map<String, String> systemProperties = Collections.emptyMap(); | 
 |   private List<File> classpath = Collections.emptyList(); | 
 |   private List<File> bootClasspath = Collections.emptyList(); | 
 |   private String mainClass; | 
 |   private List<String> programArguments = Collections.emptyList(); | 
 |   private OutputStream outRedirectStream = new ByteArrayOutputStream(); | 
 |  | 
 |   private ShellOutputReceiver hostOutput = new ShellOutputReceiver(); | 
 |  | 
 |   public static class DeviceRunnerConfigurationException extends Exception { | 
 |  | 
 |     public DeviceRunnerConfigurationException(String message) { | 
 |       super(message); | 
 |     } | 
 |   } | 
 |  | 
 |   private static class ShellOutputReceiver implements IShellOutputReceiver { | 
 |  | 
 |     private final PrintStream out; | 
 |  | 
 |     public ShellOutputReceiver() { | 
 |       this.out = System.out; | 
 |     } | 
 |  | 
 |     public ShellOutputReceiver(OutputStream out) { | 
 |       this.out = new PrintStream(out); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void addOutput(byte[] data, int offset, int length) { | 
 |       out.print(new String(Arrays.copyOfRange(data, offset, offset + length))); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void flush() { | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean isCancelled() { | 
 |       return false; | 
 |     } | 
 |   } | 
 |  | 
 |   private static class ShellOutputToStringReceiver implements IShellOutputReceiver { | 
 |  | 
 |     StringBuffer outBuffer = new StringBuffer(); | 
 |  | 
 |     @Override | 
 |     public void addOutput(byte[] data, int offset, int length) { | 
 |       outBuffer.append(new String(Arrays.copyOfRange(data, offset, offset + length))); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void flush() { | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean isCancelled() { | 
 |       return false; | 
 |     } | 
 |  | 
 |     public String getOutput() { | 
 |       return outBuffer.toString(); | 
 |     } | 
 |   } | 
 |  | 
 |   public DeviceRunner() { | 
 |     try { | 
 |       AndroidDebugBridge.init(/* clientSupport */ false); | 
 |     } catch (IllegalStateException ex) { | 
 |       // ADB was already initialized, we're fine, so just ignore. | 
 |     } | 
 |   } | 
 |  | 
 |   public DeviceRunner setVmOptions(List<String> vmOptions) { | 
 |     this.vmOptions = vmOptions; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setSystemProperties(Map<String, String> systemProperties) { | 
 |     this.systemProperties = systemProperties; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setClasspath(List<File> classpath) { | 
 |     this.classpath = classpath; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setBootClasspath(List<File> bootClasspath) { | 
 |     this.bootClasspath = bootClasspath; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setMainClass(String mainClass) { | 
 |     this.mainClass = mainClass; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setProgramArguments(List<String> programArguments) { | 
 |     this.programArguments = programArguments; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public DeviceRunner setOutputStream(OutputStream outputStream) { | 
 |     outRedirectStream = outputStream; | 
 |     return this; | 
 |   } | 
 |  | 
 |   public ToolHelper.ProcessResult run() throws DeviceRunnerConfigurationException, IOException { | 
 |  | 
 |     AndroidDebugBridge adb = initializeAdb(); | 
 |  | 
 |     IDevice[] connectedDevices = adb.getDevices(); | 
 |     if (connectedDevices.length == 0) { | 
 |       throw new DeviceRunnerConfigurationException("No device found"); | 
 |     } | 
 |  | 
 |     if (connectedDevices.length != 1) { | 
 |       throw new DeviceRunnerConfigurationException( | 
 |           "Running tests on more than one device is not yet supported. " | 
 |               + "Currently connected devices: [" | 
 |               + StringUtils.join(",", Arrays.asList(connectedDevices)) | 
 |               + "]"); | 
 |     } | 
 |  | 
 |     int exitStatus = -1; | 
 |     String uuid = java.util.UUID.randomUUID().toString(); | 
 |     IDevice device = connectedDevices[0]; | 
 |     try { | 
 |       checkDeviceRuntime(device); | 
 |  | 
 |       log("Running on device: " + device.getName()); | 
 |  | 
 |       ensureAdbRoot(device); | 
 |  | 
 |       // Remove trailing '\n' returned by emulator | 
 |       File testsRootDirFile = new File(device.getMountPoint(IDevice.MNT_DATA).replace("\n", ""), | 
 |           "r8-tests-" + uuid); | 
 |       String testsRootDir = convertToTargetPath(testsRootDirFile); | 
 |  | 
 |       String testScriptPathOnTarget = convertToTargetPath( | 
 |           new File(testsRootDirFile, TEST_SCRIPT_NAME)); | 
 |  | 
 |       ClasspathPair destFilePaths = installTestFiles(device, testsRootDirFile, | 
 |           testScriptPathOnTarget); | 
 |  | 
 |       File rootDir = new File(device.getMountPoint(IDevice.MNT_ROOT).replace("\n", "")); | 
 |  | 
 |       // Bug : exit code return by adb shell is wrong (always 0) | 
 |       // https://code.google.com/p/android/issues/detail?id=3254 | 
 |       // Use go team hack to work this around | 
 |       // https://code.google.com/p/go/source/browse/misc/arm/a | 
 |       executeShellCommand(testScriptPathOnTarget + ' ' + uuid + ' ' + Joiner.on(' ').join( | 
 |           buildCommandLine(rootDir, testsRootDir, destFilePaths.bootclasspath, | 
 |               destFilePaths.classpath)), device, new ShellOutputReceiver(outRedirectStream), | 
 |           /* maxTimeToOutputResponse = */10000); | 
 |       exitStatus = getExitStatus(device, testsRootDir); | 
 |       log("Exit status: " + exitStatus); | 
 |  | 
 |       if (exitStatus != 0) { | 
 |         System.err.println("Execution failed on device '" + device.getName() + "'"); | 
 |       } | 
 |  | 
 |     } catch (IOException t) { | 
 |       System.err.println("Error with device '" + device.getName() + "': " + t.getMessage()); | 
 |       t.printStackTrace(); | 
 |       throw t; | 
 |     } finally { | 
 |       deleteTestFiles(device, uuid); | 
 |     } | 
 |  | 
 |     // Convert target line separator to whatever host is for output comparison-based tests | 
 |     String outputAsString = outRedirectStream.toString() | 
 |         .replace("\n", ToolHelper.LINE_SEPARATOR); | 
 |  | 
 |     return new ToolHelper.ProcessResult(exitStatus, outputAsString, ""); | 
 |   } | 
 |  | 
 |   private AndroidDebugBridge initializeAdb() throws DeviceRunnerConfigurationException { | 
 |     AndroidDebugBridge adb = AndroidDebugBridge.createBridge(getAdbLocation(), false); | 
 |  | 
 |     long start = System.currentTimeMillis(); | 
 |  | 
 |     log("Initializing adb..."); | 
 |  | 
 |     while (!isAdbInitialized(adb)) { | 
 |       long timeLeft = start + ADB_CONNECTION_TIMEOUT - System.currentTimeMillis(); | 
 |       if (timeLeft <= 0) { | 
 |         break; | 
 |       } | 
 |       try { | 
 |         Thread.sleep(ADB_WAIT_STEP); | 
 |       } catch (InterruptedException e) { | 
 |         Thread.currentThread().interrupt(); | 
 |         throw new AssertionError(e); | 
 |       } | 
 |     } | 
 |  | 
 |     if (!isAdbInitialized(adb)) { | 
 |       String userDefinedPathToSdk = System.getProperty("ANDROID_SDK_HOME"); | 
 |       if (userDefinedPathToSdk != null) { | 
 |         throw new DeviceRunnerConfigurationException( | 
 |             "Adb not found. Check SDK location '" | 
 |                 + userDefinedPathToSdk | 
 |                 + "'"); | 
 |       } else { | 
 |         throw new DeviceRunnerConfigurationException( | 
 |             "Adb not found. Set either PATH or ANDROID_SDK_HOME environment variable"); | 
 |       } | 
 |     } | 
 |  | 
 |     log("Done"); | 
 |     return adb; | 
 |   } | 
 |  | 
 |   private static class ClasspathPair { | 
 |     public String[] bootclasspath; | 
 |     public String[] classpath; | 
 |  | 
 |     ClasspathPair(String[] bootclasspath, String[] classpath) { | 
 |       this.bootclasspath = bootclasspath; | 
 |       this.classpath = classpath; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return path of classpath and bootclasspath files on device | 
 |    */ | 
 |   private ClasspathPair installTestFiles(IDevice device, File testsRootDirFile, | 
 |       String testScriptPathOnTarget) throws IOException { | 
 |  | 
 |     String testsRootDir = convertToTargetPath(testsRootDirFile); | 
 |  | 
 |     String[] desFilePaths = new String[classpath.size()]; | 
 |     String[] destFileBootCpPaths = new String[bootClasspath.size()]; | 
 |  | 
 |     executeShellCommand("mkdir " + testsRootDir, device); | 
 |     executeShellCommand( | 
 |         "rm " + testsRootDir + FileListingService.FILE_SEPARATOR + "*", device); | 
 |     executePushCommand(TEST_SCRIPT_FILE.getAbsolutePath(), testScriptPathOnTarget, device); | 
 |     executeShellCommand("chmod 777 " + testScriptPathOnTarget, device); | 
 |  | 
 |     int i = 0; | 
 |     for (File f : bootClasspath) { | 
 |       destFileBootCpPaths[i] = convertToTargetPath( | 
 |           new File(testsRootDirFile, "f" + i + "_" + f.getName())); | 
 |       executePushCommand(f.getAbsolutePath(), destFileBootCpPaths[i], device); | 
 |       i++; | 
 |     } | 
 |     i = 0; | 
 |     for (File f : classpath) { | 
 |       desFilePaths[i] = convertToTargetPath( | 
 |           new File(testsRootDirFile, "f" + i + "_" + f.getName())); | 
 |       executePushCommand(f.getAbsolutePath(), desFilePaths[i], device); | 
 |       i++; | 
 |     } | 
 |     return new ClasspathPair(destFileBootCpPaths, desFilePaths); | 
 |   } | 
 |  | 
 |   private int getExitStatus(IDevice device, String testsRootDir) throws IOException { | 
 |     File exitStatusFile = createTempFile("exitStatus", ""); | 
 |     executePullCommand( | 
 |         testsRootDir + "/exitStatus", exitStatusFile.getAbsolutePath(), device); | 
 |  | 
 |     try (BufferedReader br = new BufferedReader(new FileReader(exitStatusFile))) { | 
 |       String readLine = br.readLine(); | 
 |       if (readLine == null) { | 
 |         throw new FileNotFoundException("Exit status not found at " + exitStatusFile.getPath()); | 
 |       } | 
 |       return Integer.parseInt(readLine); | 
 |     } | 
 |   } | 
 |  | 
 |   @SuppressWarnings("deprecation") | 
 |   private void executeShellCommand( | 
 |       String command, | 
 |       IDevice device, | 
 |       ShellOutputReceiver hostOutput, | 
 |       int maxTimeToOutputResponse) throws IOException { | 
 |     log("adb -s " + device.getSerialNumber() + " shell " + command); | 
 |     try { | 
 |       if (maxTimeToOutputResponse != -1) { | 
 |         device.executeShellCommand(command, hostOutput, maxTimeToOutputResponse); | 
 |       } else { | 
 |         device.executeShellCommand(command, hostOutput); | 
 |       } | 
 |     } catch (IOException | 
 |         | ShellCommandUnresponsiveException | 
 |         | TimeoutException | 
 |         | AdbCommandRejectedException e) { | 
 |       throw new IOException( | 
 |           "Failed to execute shell command: '" + command + "'", e); | 
 |     } | 
 |   } | 
 |  | 
 |   private void executeShellCommand(String command, IDevice device) | 
 |       throws IOException { | 
 |     executeShellCommand(command, device, hostOutput, -1); | 
 |   } | 
 |  | 
 |   private void executePushCommand( | 
 |       String srcFile, String destFile, IDevice device) throws IOException { | 
 |     log("adb -s " + device.getSerialNumber() + " push " + srcFile + " " + destFile); | 
 |     try { | 
 |       device.pushFile(srcFile, destFile); | 
 |     } catch (IOException | AdbCommandRejectedException | TimeoutException | SyncException e) { | 
 |       throw new IOException( | 
 |           "Unable to push file '" + srcFile + "' on device into '" + destFile + "'", e); | 
 |     } | 
 |   } | 
 |  | 
 |   private void executePullCommand( | 
 |       String srcFile, String destFile, IDevice device) throws IOException { | 
 |     log("adb -s " + device.getSerialNumber() + " pull " + srcFile + " " + destFile); | 
 |     try { | 
 |       device.pullFile(srcFile, destFile); | 
 |     } catch (IOException | AdbCommandRejectedException | TimeoutException | SyncException e) { | 
 |       throw new IOException( | 
 |           "Unable to pull file '" + srcFile + "' from device into '" + destFile + "'", e); | 
 |     } | 
 |   } | 
 |  | 
 |   private boolean isAdbInitialized(AndroidDebugBridge adb) { | 
 |     return adb.isConnected() && adb.hasInitialDeviceList(); | 
 |   } | 
 |  | 
 |   private String getAdbLocation() { | 
 |     String adbLocation = "adb"; | 
 |     String userSpecifiedSdkLocation = System.getenv("ANDROID_SDK_HOME"); | 
 |     if (userSpecifiedSdkLocation != null) { | 
 |       adbLocation = | 
 |           userSpecifiedSdkLocation | 
 |               + File.separatorChar | 
 |               + "platform-tools" | 
 |               + File.separatorChar | 
 |               + "adb"; | 
 |     } | 
 |     return adbLocation; | 
 |   } | 
 |  | 
 |  | 
 |   private void ensureAdbRoot(IDevice device) throws IOException { | 
 |     boolean isRoot; | 
 |     try { | 
 |       isRoot = device.isRoot(); | 
 |     } catch (TimeoutException | 
 |         | AdbCommandRejectedException | 
 |         | IOException | 
 |         | ShellCommandUnresponsiveException e) { | 
 |       throw new IOException( | 
 |           "Cannot fetch root status for device '" | 
 |               + device.getName() | 
 |               + "(" | 
 |               + device.getSerialNumber() | 
 |               + ")" | 
 |               + "': " | 
 |               + e.getMessage(), | 
 |           e); | 
 |     } | 
 |  | 
 |     int numberOfTries = 0; | 
 |     while (!isRoot && numberOfTries < 5) { | 
 |       try { | 
 |         isRoot = device.root(); | 
 |       } catch (TimeoutException | 
 |           | AdbCommandRejectedException | 
 |           | IOException | 
 |           | ShellCommandUnresponsiveException e1) { | 
 |         // root() seems to throw an IOException: EOF, and it tends | 
 |         // to make the subsequent call to isRoot() fail with | 
 |         // AdbCommandRejectedException: device offline, until adbd is | 
 |         // restarted as root. | 
 |       } finally { | 
 |         try { | 
 |           Thread.sleep(1000); | 
 |         } catch (InterruptedException e1) { | 
 |           Thread.currentThread().interrupt(); | 
 |         } | 
 |       } | 
 |       numberOfTries++; | 
 |     } | 
 |  | 
 |     if (!isRoot) { | 
 |       throw new IOException( | 
 |           "Cannot switch to root on device '" | 
 |               + device.getName() | 
 |               + "(" | 
 |               + device.getSerialNumber() | 
 |               + ")" | 
 |               + "'"); | 
 |     } | 
 |   } | 
 |  | 
 |   private void checkDeviceRuntime(IDevice device) | 
 |       throws DeviceRunnerConfigurationException, IOException { | 
 |     ShellOutputToStringReceiver outputToString = new ShellOutputToStringReceiver(); | 
 |     try { | 
 |       device.executeShellCommand("dalvikvm -showversion", outputToString); | 
 |       if (!outputToString.getOutput().contains(RUNTIME_NAME)) { | 
 |         throw new DeviceRunnerConfigurationException( | 
 |             "The plugged device does not run the required runtime: '" + RUNTIME_NAME + "'"); | 
 |       } | 
 |     } catch (TimeoutException | 
 |         | AdbCommandRejectedException | 
 |         | ShellCommandUnresponsiveException | 
 |         | IOException e) { | 
 |       throw new IOException("Could not check device runtime", e); | 
 |     } | 
 |   } | 
 |  | 
 |   private String convertToTargetPath(File file) { | 
 |     String path = file.getPath(); | 
 |     Path root = file.toPath().getRoot(); | 
 |     if (root != null) { | 
 |       path = path.replace(root.toString(), FileListingService.FILE_SEPARATOR); | 
 |     } | 
 |     return path.replace(File.separator, FileListingService.FILE_SEPARATOR); | 
 |   } | 
 |  | 
 |   private static File createTempFile(String prefix, String suffix) throws IOException { | 
 |     File tmp = File.createTempFile("r8-tests-" + prefix, suffix); | 
 |     tmp.deleteOnExit(); | 
 |     return tmp; | 
 |   } | 
 |  | 
 |   private void deleteTestFiles(IDevice device, String uuid) throws IOException { | 
 |     String deleteCommand = "find data -name '*r8-tests-" + uuid + "*' -exec rm -rf {} +"; | 
 |     log("adb -s " + device.getSerialNumber() + " shell " + deleteCommand); | 
 |     try { | 
 |       device.executeShellCommand(deleteCommand, hostOutput); | 
 |     } catch (TimeoutException | 
 |         | AdbCommandRejectedException | 
 |         | ShellCommandUnresponsiveException | 
 |         | IOException e) { | 
 |       throw new IOException("Error while deleting test file on device", e); | 
 |     } | 
 |   } | 
 |  | 
 |   private List<String> buildCommandLine(File rootDir, String testRootDir, | 
 |       String[] bootClasspathFiles, String[] classpathFiles) { | 
 |     List<String> result = new ArrayList<>(); | 
 |  | 
 |     result.add(convertToTargetPath(new File(rootDir, "/bin/dalvikvm"))); | 
 |  | 
 |     for (String option : vmOptions) { | 
 |       result.add(option); | 
 |     } | 
 |     for (Map.Entry<String, String> entry : systemProperties.entrySet()) { | 
 |       StringBuilder builder = new StringBuilder("-D"); | 
 |       builder.append(entry.getKey()); | 
 |       builder.append("="); | 
 |       builder.append(entry.getValue()); | 
 |       result.add(builder.toString()); | 
 |     } | 
 |  | 
 |     if (classpathFiles.length > 0) { | 
 |       result.add("-cp"); | 
 |       result.add(Joiner.on(PATH_SEPARATOR_CHAR).join( | 
 |           Arrays.asList(classpathFiles).stream() | 
 |               .map(filePath -> convertToTargetPath(new File(filePath))) | 
 |               .collect(Collectors.toList()))); | 
 |     } | 
 |     if (bootClasspathFiles.length > 0) { | 
 |       result.add("-Xbootclasspath:" + Joiner.on(PATH_SEPARATOR_CHAR).join( | 
 |           Arrays.asList(bootClasspathFiles).stream() | 
 |               .map(filePath -> convertToTargetPath(new File(filePath))) | 
 |               .collect(Collectors.toList()))); | 
 |     } | 
 |  | 
 |     if (mainClass != null) { | 
 |       result.add(mainClass); | 
 |     } | 
 |     for (String argument : programArguments) { | 
 |       result.add(argument); | 
 |     } | 
 |     return result; | 
 |   } | 
 |  | 
 |   private void log(String message) { | 
 |     if (VERBOSE) { | 
 |       System.out.println(message); | 
 |     } | 
 |   } | 
 | } |