1/*
2 * Copyright (C) 2014 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
17package dexfuzz.executors;
18
19import java.io.IOException;
20import java.io.File;
21import java.util.ArrayList;
22import java.util.List;
23import java.util.Map;
24import java.util.regex.Matcher;
25import java.util.regex.Pattern;
26
27import dexfuzz.ExecutionResult;
28import dexfuzz.Log;
29import dexfuzz.Options;
30import dexfuzz.StreamConsumer;
31
32/**
33 * Handles execution either on a remote target device, or on a local host computer.
34 */
35public class Device {
36  private boolean isHost;
37  private String deviceName;
38  private boolean usingSpecificDevice;
39  private boolean noBootImage;
40
41  private String androidHostOut;
42  private String androidProductOut;
43  private String androidData;
44
45  private boolean programPushed;
46
47  /**
48   * The constructor for a host "device".
49   */
50  public Device() {
51    this.isHost = true;
52    this.deviceName = "[HostDevice]";
53    setup();
54  }
55
56  /**
57   * The constructor for an ADB connected device.
58   */
59  public Device(String deviceName, boolean noBootImage) {
60    if (!deviceName.isEmpty()) {
61      this.deviceName = deviceName;
62      this.usingSpecificDevice = true;
63    }
64    this.noBootImage = noBootImage;
65    setup();
66  }
67
68  private String checkForEnvVar(Map<String, String> envVars, String key) {
69    if (!envVars.containsKey(key)) {
70      Log.errorAndQuit("Cannot run a fuzzed program if $" + key + " is not set!");
71    }
72    return envVars.get(key);
73  }
74
75  private String getHostCoreImagePathWithArch() {
76    // TODO: Using host currently implies x86 (see Options.java), change this when generalized.
77    assert(Options.useArchX86);
78    return androidHostOut + "/framework/x86/core.art";
79  }
80
81  private String getHostCoreImagePathNoArch() {
82    return androidHostOut + "/framework/core.art";
83  }
84
85  private void setup() {
86    programPushed = false;
87
88    Map<String, String> envVars = System.getenv();
89    androidProductOut = checkForEnvVar(envVars, "ANDROID_PRODUCT_OUT");
90    androidHostOut = checkForEnvVar(envVars, "ANDROID_HOST_OUT");
91
92    if (Options.executeOnHost) {
93      File coreImage = new File(getHostCoreImagePathWithArch());
94      if (!coreImage.exists()) {
95        Log.errorAndQuit("Host core image not found at " + coreImage.getPath()
96            + ". Did you forget to build it?");
97      }
98    }
99    if (!isHost) {
100      // Create temporary consumers for the initial test.
101      StreamConsumer outputConsumer = new StreamConsumer();
102      outputConsumer.start();
103      StreamConsumer errorConsumer = new StreamConsumer();
104      errorConsumer.start();
105
106      // Check for ADB.
107      try {
108        ProcessBuilder pb = new ProcessBuilder();
109        pb.command("adb", "devices");
110        Process process = pb.start();
111        int exitValue = process.waitFor();
112        if (exitValue != 0) {
113          Log.errorAndQuit("Problem executing ADB - is it in your $PATH?");
114        }
115      } catch (IOException e) {
116        Log.errorAndQuit("IOException when executing ADB, is it working?");
117      } catch (InterruptedException e) {
118        Log.errorAndQuit("InterruptedException when executing ADB, is it working?");
119      }
120
121      // Check we can run something on ADB.
122      ExecutionResult result = executeCommand("true", true, outputConsumer, errorConsumer);
123      if (result.getFlattenedAll().contains("device not found")) {
124        Log.errorAndQuit("Couldn't connect to specified ADB device: " + deviceName);
125      }
126
127      outputConsumer.shutdown();
128      errorConsumer.shutdown();
129    } else {
130      androidData = checkForEnvVar(envVars, "ANDROID_DATA");
131    }
132  }
133
134  /**
135   * Get the name that would be provided to adb -s to communicate specifically with this device.
136   */
137  public String getName() {
138    assert(!isHost);
139    return deviceName;
140  }
141
142  public boolean isHost() {
143    return isHost;
144  }
145
146  public boolean isUsingSpecificDevice() {
147    return usingSpecificDevice;
148  }
149
150  /**
151   * Certain AOSP builds of Android may not have a full boot.art built. This will be set if
152   * we use --no-boot-image, and is used by Executors when deciding the arguments for dalvikvm
153   * and dex2oat when performing host-side verification.
154   */
155  public boolean noBootImageAvailable() {
156    return noBootImage;
157  }
158
159  /**
160   * Get the command prefix for this device if we want to use adb shell.
161   */
162  public String getExecutionShellPrefix() {
163    if (isHost) {
164      return "";
165    }
166    return getExecutionPrefixWithAdb("shell");
167  }
168
169  /**
170   * Get any extra flags required to execute ART on the host.
171   */
172  public String getHostExecutionFlags() {
173    return String.format("-Xnorelocate -Ximage:%s", getHostCoreImagePathNoArch());
174  }
175
176  public String getAndroidHostOut() {
177    return androidHostOut;
178  }
179
180  public String getAndroidProductOut() {
181    return androidProductOut;
182  }
183
184  public ExecutionResult executeCommand(String command, boolean captureOutput) {
185    assert(!captureOutput);
186    return executeCommand(command, captureOutput, null, null);
187  }
188
189  public ExecutionResult executeCommand(String command, boolean captureOutput,
190      StreamConsumer outputConsumer, StreamConsumer errorConsumer) {
191
192    ExecutionResult result = new ExecutionResult();
193
194    Log.info("Executing: " + command);
195
196    try {
197      ProcessBuilder processBuilder = new ProcessBuilder(splitCommand(command));
198      processBuilder.environment().put("ANDROID_ROOT", androidHostOut);
199      if (Options.executeOnHost) {
200        processBuilder.environment().put("ANDROID_DATA", androidData);
201      }
202      Process process = processBuilder.start();
203
204      if (captureOutput) {
205        // Give the streams to the StreamConsumers.
206        outputConsumer.giveStreamAndStartConsuming(process.getInputStream());
207        errorConsumer.giveStreamAndStartConsuming(process.getErrorStream());
208      }
209
210      // Wait until the process is done - the StreamConsumers will keep the
211      // buffers drained, so this shouldn't block indefinitely.
212      // Get the return value as well.
213      result.returnValue = process.waitFor();
214
215      Log.info("Return value: " + result.returnValue);
216
217      if (captureOutput) {
218        // Tell the StreamConsumers to stop consuming, and wait for them to finish
219        // so we know we have all of the output.
220        outputConsumer.processFinished();
221        errorConsumer.processFinished();
222        result.output = outputConsumer.getOutput();
223        result.error = errorConsumer.getOutput();
224
225        // Always explicitly indicate the return code in the text output now.
226        // NB: adb shell doesn't actually return exit codes currently, but this will
227        // be useful if/when it does.
228        result.output.add("RETURN CODE: " + result.returnValue);
229      }
230
231    } catch (IOException e) {
232      Log.errorAndQuit("ExecutionResult.execute() caught an IOException");
233    } catch (InterruptedException e) {
234      Log.errorAndQuit("ExecutionResult.execute() caught an InterruptedException");
235    }
236
237    return result;
238  }
239
240  /**
241   * Splits command respecting single quotes.
242   */
243  private List<String> splitCommand(String command) {
244    List<String> ret = new ArrayList<String>();
245    Matcher m = Pattern.compile("(\'[^\']+\'| *[^ ]+ *)").matcher(command);
246    while (m.find())
247      ret.add(m.group(1).trim().replace("\'", ""));
248    return ret;
249  }
250
251  private String getExecutionPrefixWithAdb(String command) {
252    if (usingSpecificDevice) {
253      return String.format("adb -s %s %s ", deviceName, command);
254    } else {
255      return String.format("adb %s ", command);
256    }
257  }
258
259  private String getCacheLocation(Architecture architecture) {
260    String cacheLocation = "";
261    if (isHost) {
262      cacheLocation = androidData + "/dalvik-cache/" + architecture.asString() + "/";
263    } else {
264      cacheLocation = "/data/dalvik-cache/" + architecture.asString() + "/";
265    }
266    return cacheLocation;
267  }
268
269  private String getOatFileName(String testLocation, String programName) {
270    // Converts e.g. /data/art-test/file.dex to data@art-test@file.dex
271    return (testLocation.replace("/", "@").substring(1) + "@" + programName);
272  }
273
274  public void cleanCodeCache(Architecture architecture, String testLocation, String programName) {
275    String command = getExecutionPrefixWithAdb("shell") + "rm -f " + getCacheLocation(architecture)
276        + getOatFileName(testLocation, programName);
277    executeCommand(command, false);
278  }
279
280  public void pushProgramToDevice(String programName, String testLocation) {
281    assert(!isHost);
282    if (!programPushed) {
283      String command = getExecutionPrefixWithAdb("push") + programName + " " + testLocation;
284      ExecutionResult result = executeCommand(command, false);
285      if (result.returnValue != 0) {
286        Log.errorAndQuit("Could not ADB PUSH program to device.");
287      }
288      programPushed = true;
289    }
290  }
291
292  public void resetProgramPushed() {
293    programPushed = false;
294  }
295}
296