|  | // 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.bisect; | 
|  |  | 
|  | import com.android.tools.r8.OutputMode; | 
|  | import com.android.tools.r8.ProgramConsumer; | 
|  | import com.android.tools.r8.StringConsumer; | 
|  | import com.android.tools.r8.bisect.BisectOptions.Result; | 
|  | import com.android.tools.r8.dex.ApplicationReader; | 
|  | import com.android.tools.r8.dex.ApplicationWriter; | 
|  | import com.android.tools.r8.errors.CompilationError; | 
|  | import com.android.tools.r8.graph.AppInfo; | 
|  | import com.android.tools.r8.graph.AppView; | 
|  | import com.android.tools.r8.graph.DexApplication; | 
|  | import com.android.tools.r8.graph.DexProgramClass; | 
|  | import com.android.tools.r8.graph.GraphLens; | 
|  | import com.android.tools.r8.graph.InitClassLens; | 
|  | import com.android.tools.r8.naming.NamingLens; | 
|  | import com.android.tools.r8.utils.AndroidApp; | 
|  | import com.android.tools.r8.utils.AndroidAppConsumers; | 
|  | import com.android.tools.r8.utils.InternalOptions; | 
|  | import com.android.tools.r8.utils.Timing; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.io.CharStreams; | 
|  | import java.io.IOException; | 
|  | import java.io.InputStream; | 
|  | import java.io.InputStreamReader; | 
|  | import java.nio.charset.StandardCharsets; | 
|  | import java.nio.file.Files; | 
|  | import java.nio.file.Path; | 
|  | import java.util.ArrayList; | 
|  | import java.util.List; | 
|  | import java.util.concurrent.ExecutionException; | 
|  | import java.util.concurrent.ExecutorService; | 
|  | import java.util.concurrent.Executors; | 
|  |  | 
|  | public class Bisect { | 
|  |  | 
|  | private final BisectOptions options; | 
|  | private final Timing timing = new Timing("bisect"); | 
|  |  | 
|  | public interface Command { | 
|  |  | 
|  | Result apply(DexApplication application) throws Exception; | 
|  | } | 
|  |  | 
|  | private static class StreamReader implements Runnable { | 
|  |  | 
|  | private final InputStream stream; | 
|  | private String result; | 
|  |  | 
|  | public StreamReader(InputStream stream) { | 
|  | this.stream = stream; | 
|  | } | 
|  |  | 
|  | public String getResult() { | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void run() { | 
|  | try { | 
|  | result = CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8)); | 
|  | stream.close(); | 
|  | } catch (IOException e) { | 
|  | result = "Failed reading result for stream " + stream; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public Bisect(BisectOptions options) { | 
|  | this.options = options; | 
|  | } | 
|  |  | 
|  | public static DexProgramClass run(BisectState state, Command command, Path output, | 
|  | ExecutorService executor) | 
|  | throws Exception { | 
|  | while (true) { | 
|  | DexApplication app = state.bisect(); | 
|  | state.write(); | 
|  | if (app == null) { | 
|  | return state.getFinalClass(); | 
|  | } | 
|  | if (command == null) { | 
|  | writeApp(app, output, executor); | 
|  | System.out.println("Bisecting completed with build in " + output + "/"); | 
|  | System.out.println("Continue bisection by passing either --" | 
|  | + BisectOptions.RESULT_GOOD_FLAG + " or --" | 
|  | + BisectOptions.RESULT_BAD_FLAG); | 
|  | return null; | 
|  | } | 
|  | state.setPreviousResult(command.apply(app)); | 
|  | } | 
|  | } | 
|  |  | 
|  | public DexProgramClass run() throws Exception { | 
|  | // Setup output directory (or write to a temp dir). | 
|  | Path output; | 
|  | if (options.output != null) { | 
|  | output = options.output; | 
|  | } else { | 
|  | output = Files.createTempDirectory("bisect"); | 
|  | } | 
|  |  | 
|  | ExecutorService executor = Executors.newWorkStealingPool(); | 
|  | try { | 
|  | InternalOptions internal = new InternalOptions(); | 
|  | DexApplication goodApp = readApp(options.goodBuild, internal, executor); | 
|  | DexApplication badApp = readApp(options.badBuild, internal, executor); | 
|  |  | 
|  | Path stateFile = | 
|  | options.stateFile != null ? options.stateFile : output.resolve("bisect.state"); | 
|  |  | 
|  | // Setup initial (or saved) bisection state. | 
|  | BisectState state = new BisectState(goodApp, badApp, stateFile); | 
|  | if (options.stateFile != null) { | 
|  | state.read(); | 
|  | } | 
|  |  | 
|  | // If a "previous" result is supplied on the command line, record it. | 
|  | if (options.result != Result.UNKNOWN) { | 
|  | state.setPreviousResult(options.result); | 
|  | } | 
|  |  | 
|  | // Setup post-build command. | 
|  | Command command = null; | 
|  | if (options.command != null) { | 
|  | command = | 
|  | (application) -> { | 
|  | writeApp(application, output, executor); | 
|  | return runCommand(options.command, options.goodBuild, options.badBuild, output); | 
|  | }; | 
|  | } | 
|  |  | 
|  | // Run bisection. | 
|  | return run(state, command, output, executor); | 
|  | } finally { | 
|  | executor.shutdown(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static Result runCommand(Path command, Path good, Path bad, Path output) | 
|  | throws IOException { | 
|  | List<String> args = new ArrayList<>(); | 
|  | args.add("/bin/bash"); | 
|  | args.add(command.toString()); | 
|  | args.addAll(ImmutableList.of(good.toString(), bad.toString(), output.toString())); | 
|  | System.out.println("Running cmd: " + String.join(" ", args)); | 
|  | ProcessBuilder builder = new ProcessBuilder(args); | 
|  | Process process = builder.start(); | 
|  | StreamReader stdoutReader = new StreamReader(process.getInputStream()); | 
|  | StreamReader stderrReader = new StreamReader(process.getErrorStream()); | 
|  | Thread stdoutThread = new Thread(stdoutReader); | 
|  | Thread stderrThread = new Thread(stderrReader); | 
|  | stdoutThread.start(); | 
|  | stderrThread.start(); | 
|  | try { | 
|  | process.waitFor(); | 
|  | stdoutThread.join(); | 
|  | stderrThread.join(); | 
|  | } catch (InterruptedException e) { | 
|  | throw new RuntimeException("Execution interrupted", e); | 
|  | } | 
|  | System.out.print("OUT:\n" + stdoutReader.getResult()); | 
|  | System.out.print("ERR:\n" + stderrReader.getResult()); | 
|  | int result = process.exitValue(); | 
|  | if (result == 0) { | 
|  | return Result.GOOD; | 
|  | } else if (result == 1) { | 
|  | return Result.BAD; | 
|  | } | 
|  | System.out.println("Failed to run command " + args); | 
|  | System.out.println("Exit code: " + result + " (expected 0 for good, 1 for bad)"); | 
|  | throw new CompilationError("Failed to run command " + args); | 
|  | } | 
|  |  | 
|  | private DexApplication readApp(Path apk, InternalOptions options, ExecutorService executor) | 
|  | throws IOException, ExecutionException { | 
|  | AndroidApp app = AndroidApp.builder().addProgramFiles(apk).build(); | 
|  | return new ApplicationReader(app, options, timing).read(executor); | 
|  | } | 
|  |  | 
|  | private static void writeApp(DexApplication app, Path output, ExecutorService executor) | 
|  | throws IOException, ExecutionException { | 
|  | InternalOptions options = app.options; | 
|  | // Save the original consumers so they can be unwrapped after write. | 
|  | ProgramConsumer programConsumer = options.programConsumer; | 
|  | StringConsumer proguardMapConsumer = options.proguardMapConsumer; | 
|  | AndroidAppConsumers compatSink = new AndroidAppConsumers(options); | 
|  | ApplicationWriter writer = | 
|  | new ApplicationWriter( | 
|  | AppView.createForD8(AppInfo.createInitialAppInfo(app)), | 
|  | null, | 
|  | GraphLens.getIdentityLens(), | 
|  | InitClassLens.getThrowingInstance(), | 
|  | NamingLens.getIdentityLens(), | 
|  | null); | 
|  | writer.write(executor); | 
|  | options.signalFinishedToConsumers(); | 
|  | compatSink.build().writeToDirectory(output, OutputMode.DexIndexed); | 
|  | // Restore original consumers. | 
|  | options.programConsumer = programConsumer; | 
|  | options.proguardMapConsumer = proguardMapConsumer; | 
|  | } | 
|  |  | 
|  | public static DexProgramClass run(BisectOptions options) throws Exception { | 
|  | return new Bisect(options).run(); | 
|  | } | 
|  |  | 
|  | public static void main(String[] args) throws Exception { | 
|  | BisectOptions options; | 
|  | try { | 
|  | options = BisectOptions.parse(args); | 
|  | } catch (CompilationError e) { | 
|  | System.err.println(e.getMessage()); | 
|  | BisectOptions.printHelp(System.err); | 
|  | return; | 
|  | } | 
|  | if (options == null) { | 
|  | return; | 
|  | } | 
|  | DexProgramClass clazz = Bisect.run(options); | 
|  | if (clazz != null) { | 
|  | System.out.println("Bisection found final bad class " + clazz); | 
|  | } | 
|  | } | 
|  | } |