• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.tools.r8wrappers.retrace;
18 
19 import com.android.tools.r8.Diagnostic;
20 import com.android.tools.r8.DiagnosticsHandler;
21 import com.android.tools.r8.references.ClassReference;
22 import com.android.tools.r8.references.Reference;
23 import com.android.tools.r8.retrace.ProguardMapProducer;
24 import com.android.tools.r8.retrace.RetraceStackTraceContext;
25 import com.android.tools.r8.retrace.RetracedClassReference;
26 import com.android.tools.r8.retrace.RetracedMethodReference;
27 import com.android.tools.r8.retrace.Retracer;
28 import java.io.BufferedReader;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.lang.ProcessBuilder.Redirect;
33 import java.net.URISyntaxException;
34 import java.nio.file.FileVisitResult;
35 import java.nio.file.FileVisitor;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.nio.file.StandardOpenOption;
40 import java.nio.file.attribute.BasicFileAttributes;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collections;
44 import java.util.Comparator;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.OptionalInt;
50 import java.util.function.BiConsumer;
51 import java.util.stream.Collectors;
52 import java.util.stream.Stream;
53 
54 public class RetraceWrapper {
55 
56   /** Default paths to search for mapping files. */
57   private static final List<String> AOSP_MAP_SEARCH_PATHS =
58       Collections.singletonList("out/target/common/obj/APPS");
59 
60   private static final String USAGE =
61       String.join(
62           System.lineSeparator(),
63           "Usage: retrace [<option>]* [<file>]",
64           "where <file> is the file to retrace (default stdin)",
65           "and for retracing build server artifacts <option>s are:",
66           "  --bid <build id>            # Build identifier, e.g., 1234 or P1234",
67           "  --target <target>           # Build target name, e.g., coral-userdebug",
68           "  --branch <branch>           # Branch, e.g., master (only needed when bid is not a",
69           "                              # build number)",
70           "or for controlling map lookup/location <option>s are:",
71           "  --default-map <file/app>    # Default map to retrace lines that don't auto-identify.",
72           "                              # The argument can be a local file or it can be any",
73           "                              # unique substring of a map path found in the map-table.",
74           "  --map-search-path <path>    # Path to search for mappings that support auto-identify.",
75           "                              # Separate <path> entries by colon ':'.",
76           "                              # Default '"
77               + String.join(":", AOSP_MAP_SEARCH_PATHS)
78               + "'.",
79           "other supported <option>s are:",
80           "  --print-map-table           # Print the table of identified mapping files and exit.",
81           "  --cwd-relative-search-paths # When this flag is set, the search paths given in",
82           "                              # the --map-search-path flag are assumed to be relative",
83           "                              # to the current directory. Otherwise, these paths are",
84           "                              # assumed to be relative to the root of the Android",
85           "                              # checkout.",
86           "  --temp <path>               # Use <path> as the temporary directory without cleanup.",
87           "                              # This will cause build artifacts to be cached in temp.",
88           "  -h, --help                  # Print this message.");
89 
90   private static class ForwardingDiagnosticsHander implements DiagnosticsHandler {
91     @Override
error(Diagnostic error)92     public void error(Diagnostic error) {
93       throw RetraceWrapper.error(error.getDiagnosticMessage());
94     }
95 
96     @Override
warning(Diagnostic warning)97     public void warning(Diagnostic warning) {
98       RetraceWrapper.warning(warning.getDiagnosticMessage());
99     }
100 
101     @Override
info(Diagnostic info)102     public void info(Diagnostic info) {
103       RetraceWrapper.info(info.getDiagnosticMessage());
104     }
105   }
106 
107   private interface LazyRetracer {
getMapLocation()108     String getMapLocation();
109 
getRetracer(Path tempDir)110     Retracer getRetracer(Path tempDir) throws Exception;
111   }
112 
113   private static class LocalLazyRetracer implements LazyRetracer {
114     final MapInfo mapInfo;
115     final Path mapPath;
116 
117     private Retracer lazyRetracer = null;
118 
LocalLazyRetracer(MapInfo mapInfo, Path mapPath)119     public LocalLazyRetracer(MapInfo mapInfo, Path mapPath) {
120       this.mapInfo = mapInfo;
121       this.mapPath = mapPath;
122     }
123 
124     @Override
getMapLocation()125     public String getMapLocation() {
126       return mapPath.toString();
127     }
128 
129     @Override
getRetracer(Path tempDir)130     public Retracer getRetracer(Path tempDir) throws Exception {
131       if (lazyRetracer == null) {
132         lazyRetracer =
133             Retracer.createDefault(
134                 ProguardMapProducer.fromPath(mapPath), new ForwardingDiagnosticsHander());
135       }
136       return lazyRetracer;
137     }
138   }
139 
140   private static class RemoteLazyRetracer implements LazyRetracer {
141 
142     private final MapInfo mapInfo;
143     private final BuildInfo buildInfo;
144     private final String mappingFile;
145     private final String zipEntry;
146 
RemoteLazyRetracer( MapInfo mapInfo, BuildInfo buildInfo, String mappingFile, String zipEntry)147     public RemoteLazyRetracer(
148         MapInfo mapInfo, BuildInfo buildInfo, String mappingFile, String zipEntry) {
149       this.mapInfo = mapInfo;
150       this.buildInfo = buildInfo;
151       this.mappingFile = mappingFile;
152       this.zipEntry = zipEntry;
153     }
154 
155     private Retracer lazyRetracer = null;
156 
157     @Override
getMapLocation()158     public String getMapLocation() {
159       return String.join(" ", fetchArtifactCommand(buildInfo, mappingFile, zipEntry));
160     }
161 
162     @Override
getRetracer(Path tempDir)163     public Retracer getRetracer(Path tempDir) throws IOException, InterruptedException {
164       if (lazyRetracer == null) {
165         Path mapFile = fetchArtifact(buildInfo, mappingFile, zipEntry, tempDir);
166         lazyRetracer =
167             Retracer.createDefault(
168                 ProguardMapProducer.fromPath(mapFile), new ForwardingDiagnosticsHander());
169       }
170       return lazyRetracer;
171     }
172   }
173 
174   private static class BuildInfo {
175     final String id;
176     final String target;
177     final String branch;
178 
BuildInfo(String id, String target, String branch)179     public BuildInfo(String id, String target, String branch) {
180       this.id = id;
181       this.target = target;
182       this.branch = branch;
183     }
184 
getMetaMappingFileSuffix()185     public String getMetaMappingFileSuffix() {
186       return "-proguard-dict-mapping-" + id + ".textproto";
187     }
188 
getMappingFileSuffix()189     public String getMappingFileSuffix() {
190       return "-proguard-dict-" + id + ".zip";
191     }
192 
193     @Override
toString()194     public String toString() {
195       return "bid=" + id + ", target=" + target + (branch == null ? "" : ", branch=" + branch);
196     }
197   }
198 
199   private static class MapInfo {
200     final String id;
201     final String hash;
202 
MapInfo(String id, String hash)203     public MapInfo(String id, String hash) {
204       assert id != null;
205       assert hash != null;
206       this.id = id;
207       this.hash = hash;
208     }
209 
210     @Override
equals(Object other)211     public boolean equals(Object other) {
212       if (this == other) {
213         return true;
214       }
215       if (other == null || getClass() != other.getClass()) {
216         return false;
217       }
218       MapInfo otherMapInfo = (MapInfo) other;
219       return Objects.equals(id, otherMapInfo.id) && Objects.equals(hash, otherMapInfo.hash);
220     }
221 
222     @Override
hashCode()223     public int hashCode() {
224       return Objects.hash(id, hash);
225     }
226 
227     @Override
toString()228     public String toString() {
229       return "MapInfo{" + "id='" + id + '\'' + ", hash='" + hash + '\'' + '}';
230     }
231   }
232 
233   /** Representation of a line with a hole, ala "<prefix><hole><suffix>". */
234   private static class LineWithHole {
235     final String line;
236     final int start;
237     final int end;
238 
LineWithHole(String line, int start, int end)239     public LineWithHole(String line, int start, int end) {
240       this.line = line;
241       this.start = start;
242       this.end = end;
243     }
244 
plug(String string)245     public String plug(String string) {
246       return line.substring(0, start) + string + line.substring(end);
247     }
248   }
249 
250   /** Parsed exception header line, such as "Caused by: <exception>". */
251   private static class ExceptionLine extends LineWithHole {
252     final ClassReference exception;
253 
ExceptionLine(String line, int start, int end, ClassReference exception)254     public ExceptionLine(String line, int start, int end, ClassReference exception) {
255       super(line, start, end);
256       this.exception = exception;
257     }
258   }
259 
260   /** Parsed frame line, such as "at <class>.<method>(<source-file>:<line>)". */
261   private static class FrameLine extends LineWithHole {
262     final ClassReference clazz;
263     final String methodName;
264     final String sourceFile;
265     final OptionalInt lineNumber;
266 
FrameLine( String line, int start, int end, ClassReference clazz, String methodName, String sourceFile, OptionalInt lineNumber)267     public FrameLine(
268         String line,
269         int start,
270         int end,
271         ClassReference clazz,
272         String methodName,
273         String sourceFile,
274         OptionalInt lineNumber) {
275       super(line, start, end);
276       this.clazz = clazz;
277       this.methodName = methodName;
278       this.sourceFile = sourceFile;
279       this.lineNumber = lineNumber;
280     }
281   }
282 
283   /** An immutable linked list of the result lines so that a result tree can be created. */
284   private static class ResultNode {
285     final ResultNode parent;
286     final String line;
287 
ResultNode(ResultNode parent, String line)288     public ResultNode(ResultNode parent, String line) {
289       this.parent = parent;
290       this.line = line;
291     }
292 
print()293     public void print() {
294       if (parent != null) {
295         parent.print();
296       }
297       System.out.println(line);
298     }
299   }
300 
301   /**
302    * Indication that a line is the start of an escaping stack trace.
303    *
304    * <p>Note that this does not identify an exception that is directly printed with, e.g.,
305    * Throwable.printStackTrace(), but only one that exits the runtime. That should generally catch
306    * what we need, but could be refined to match ':' which is the only other indicator.
307    */
308   private static final String ESCAPING_EXCEPTION_MARKER = "Exception in thread \"";
309 
310   /** Indication that a line is the start of a "caused by" stack trace. */
311   private static final String CAUSED_BY_EXCEPTION_MARKER = "Caused by: ";
312 
313   /** Indication that a line is the start of a "suppressed" stack trace. */
314   private static final String SUPPRESSED_EXCEPTION_MARKER = "Suppressed: ";
315 
316   /** Start of the source file for any R8 build withing AOSP. */
317   // TODO(zerny): Should this be a configurable prefix?
318   private static final String AOSP_SF_MARKER = "go/retraceme ";
319 
320   /** Start of the source file for any R8 compiler build. */
321   private static final String R8_SF_MARKER = "R8_";
322 
323   /** Mapping file header indicating the id of mapping file. */
324   private static final String MAP_ID_HEADER_MARKER = "# pg_map_id: ";
325 
326   /** Mapping file header indicating the hash of mapping file. */
327   private static final String MAP_HASH_HEADER_MARKER = "# pg_map_hash: SHA-256 ";
328 
329   /** Map of cached/lazy retracer instances for maps found in the local AOSP build. */
330   private static final Map<String, LazyRetracer> RETRACERS = new HashMap<>();
331 
332   private static final List<String> PENDING_MESSAGES = new ArrayList<>();
333 
flushPendingMessages()334   private static void flushPendingMessages() {
335     PENDING_MESSAGES.forEach(System.err::println);
336   }
337 
info(String message)338   private static void info(String message) {
339     PENDING_MESSAGES.add("Info: " + message);
340   }
341 
warning(String message)342   private static void warning(String message) {
343     PENDING_MESSAGES.add("Warning: " + message);
344   }
345 
error(String message)346   private static RuntimeException error(String message) {
347     flushPendingMessages();
348     throw new RuntimeException(message);
349   }
350 
readMapHeaderInfo(Path path)351   private static MapInfo readMapHeaderInfo(Path path) throws IOException {
352     String mapId = null;
353     String mapHash = null;
354     try (BufferedReader reader = Files.newBufferedReader(path)) {
355       while (true) {
356         String line = reader.readLine();
357         if (line == null || !line.startsWith("#")) {
358           break;
359         }
360         if (mapId == null) {
361           mapId = tryParseMapIdHeader(line);
362         }
363         if (mapHash == null) {
364           mapHash = tryParseMapHashHeader(line);
365         }
366       }
367     }
368     if (mapId != null && mapHash != null) {
369       return new MapInfo(mapId, mapHash);
370     }
371     return null;
372   }
373 
getProjectRoot()374   private static Path getProjectRoot() throws URISyntaxException {
375     // The retrace.jar should be located in out/[soong/]host/<platform>/framework/retrace.jar
376     Path hostPath = Paths.get("out", "host");
377     Path hostSoongPath = Paths.get("out", "soong");
378     Path retraceJarPath =
379         Paths.get(RetraceWrapper.class.getProtectionDomain().getCodeSource().getLocation().toURI());
380     for (Path current = retraceJarPath; current != null; current = current.getParent()) {
381       if (current.endsWith(hostPath) || current.endsWith(hostSoongPath)) {
382         return current.getParent().getParent();
383       }
384     }
385     info(
386         "Unable to determine the project root based on the retrace.jar location: "
387             + retraceJarPath);
388     return null;
389   }
390 
getRetracerForAosp(String sourceFile)391   private static LazyRetracer getRetracerForAosp(String sourceFile) {
392     MapInfo stackLineInfo = tryParseSourceFileMarkerForAosp(sourceFile);
393     return stackLineInfo == null ? null : RETRACERS.get(stackLineInfo.id);
394   }
395 
getRetracerForR8(String sourceFile)396   private static LazyRetracer getRetracerForR8(String sourceFile) {
397     MapInfo stackLineInfo = tryParseSourceFileMarkerForR8(sourceFile);
398     if (stackLineInfo == null) {
399       return null;
400     }
401     LazyRetracer retracer = RETRACERS.get(stackLineInfo.id);
402     if (retracer == null) {
403       // TODO(zerny): Lookup the mapping file in the R8 cloud storage bucket.
404       info("Could not identify a mapping file for lines with R8 tag: " + stackLineInfo);
405     }
406     return retracer;
407   }
408 
tryParseMapIdHeader(String line)409   private static String tryParseMapIdHeader(String line) {
410     return tryParseMapHeaderLine(line, MAP_ID_HEADER_MARKER);
411   }
412 
tryParseMapHashHeader(String line)413   private static String tryParseMapHashHeader(String line) {
414     return tryParseMapHeaderLine(line, MAP_HASH_HEADER_MARKER);
415   }
416 
tryParseMapHeaderLine(String line, String headerMarker)417   private static String tryParseMapHeaderLine(String line, String headerMarker) {
418     if (line.startsWith(headerMarker)) {
419       return line.substring(headerMarker.length());
420     }
421     return null;
422   }
423 
tryParseFrameLine(String line)424   private static FrameLine tryParseFrameLine(String line) {
425     String atMarker = "at ";
426     int atIndex = line.indexOf(atMarker);
427     if (atIndex < 0) {
428       return null;
429     }
430     int parenStartIndex = line.indexOf('(', atIndex);
431     if (parenStartIndex < 0) {
432       return null;
433     }
434     int parenEndIndex = line.indexOf(')', parenStartIndex);
435     if (parenEndIndex < 0) {
436       return null;
437     }
438     int classAndMethodSeperatorIndex = line.lastIndexOf('.', parenStartIndex);
439     if (classAndMethodSeperatorIndex < 0) {
440       return null;
441     }
442     int classStartIndex = atIndex + atMarker.length();
443     String clazz = line.substring(classStartIndex, classAndMethodSeperatorIndex);
444     String method = line.substring(classAndMethodSeperatorIndex + 1, parenStartIndex);
445     // Source file and line may or may not be present.
446     int sourceAndLineSeperatorIndex = line.lastIndexOf(':', parenEndIndex);
447     String sourceFile;
448     OptionalInt lineNumber;
449     if (parenStartIndex < sourceAndLineSeperatorIndex) {
450       sourceFile = line.substring(parenStartIndex + 1, sourceAndLineSeperatorIndex);
451       try {
452         lineNumber =
453             OptionalInt.of(
454                 Integer.parseInt(line.substring(sourceAndLineSeperatorIndex + 1, parenEndIndex)));
455       } catch (NumberFormatException e) {
456         lineNumber = OptionalInt.empty();
457       }
458     } else {
459       sourceFile = line.substring(parenStartIndex + 1, parenEndIndex);
460       lineNumber = OptionalInt.empty();
461     }
462     return new FrameLine(
463         line,
464         classStartIndex,
465         parenEndIndex + 1,
466         Reference.classFromTypeName(clazz),
467         method,
468         sourceFile,
469         lineNumber);
470   }
471 
indexOfExceptionStart(String line)472   private static int indexOfExceptionStart(String line) {
473     int i = line.indexOf(ESCAPING_EXCEPTION_MARKER);
474     if (i >= 0) {
475       int start = line.indexOf("\" ", i + ESCAPING_EXCEPTION_MARKER.length());
476       if (start > 0) {
477         return start;
478       }
479     }
480     i = line.indexOf(CAUSED_BY_EXCEPTION_MARKER);
481     if (i >= 0) {
482       return i + CAUSED_BY_EXCEPTION_MARKER.length();
483     }
484     i = line.indexOf(SUPPRESSED_EXCEPTION_MARKER);
485     if (i >= 0) {
486       return i + SUPPRESSED_EXCEPTION_MARKER.length();
487     }
488     return -1;
489   }
490 
tryParseExceptionLine(String line)491   private static ExceptionLine tryParseExceptionLine(String line) {
492     int start = indexOfExceptionStart(line);
493     if (start < 0) {
494       return null;
495     }
496     int end = line.indexOf(':', start);
497     if (end < 0) {
498       return null;
499     }
500     String exception = line.substring(start, end);
501     return new ExceptionLine(line, start, end, Reference.classFromTypeName(exception));
502   }
503 
tryParseSourceFileMarkerForAosp(String sourceFile)504   private static MapInfo tryParseSourceFileMarkerForAosp(String sourceFile) {
505     if (!sourceFile.startsWith(AOSP_SF_MARKER)) {
506       return null;
507     }
508     int hashStart = AOSP_SF_MARKER.length();
509     String mapHash = sourceFile.substring(hashStart);
510     // Currently, app builds use the map-hash as the build id.
511     return new MapInfo(mapHash, mapHash);
512   }
513 
tryParseSourceFileMarkerForR8(String sourceFile)514   private static MapInfo tryParseSourceFileMarkerForR8(String sourceFile) {
515     if (!sourceFile.startsWith(R8_SF_MARKER)) {
516       return null;
517     }
518     int versionStart = R8_SF_MARKER.length();
519     int mapHashStart = sourceFile.indexOf('_', versionStart) + 1;
520     if (mapHashStart <= 0) {
521       return null;
522     }
523     String version = sourceFile.substring(versionStart, mapHashStart - 1);
524     String mapHash = sourceFile.substring(mapHashStart);
525     return new MapInfo(version, mapHash);
526   }
527 
printIdentityStackTrace(ExceptionLine exceptionLine, List<FrameLine> frames)528   private static void printIdentityStackTrace(ExceptionLine exceptionLine, List<FrameLine> frames) {
529     if (exceptionLine != null) {
530       System.out.println(exceptionLine.line);
531     }
532     frames.forEach(frame -> System.out.println(frame.line));
533   }
534 
retrace(InputStream stream, LazyRetracer defaultRetracer, Path tempDir)535   private static void retrace(InputStream stream, LazyRetracer defaultRetracer, Path tempDir)
536       throws Exception {
537     BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
538     String currentLine = reader.readLine();
539     List<FrameLine> frames = new ArrayList<>();
540     while (currentLine != null) {
541       ExceptionLine exceptionLine = tryParseExceptionLine(currentLine);
542       if (exceptionLine != null) {
543         currentLine = reader.readLine();
544         if (currentLine == null) {
545           // Reached end-of-file and we can't retrace the exception. Flush and exit.
546           printIdentityStackTrace(exceptionLine, Collections.emptyList());
547           return;
548         }
549       }
550       FrameLine topFrameLine = tryParseFrameLine(currentLine);
551       if (topFrameLine == null) {
552         // The line is not a frame so we can't retrace it. Flush and continue on the next line.
553         printIdentityStackTrace(exceptionLine, Collections.emptyList());
554         System.out.println(currentLine);
555         currentLine = reader.readLine();
556         continue;
557       }
558       // Collect all subsequent lines with the same source file info.
559       FrameLine frame = topFrameLine;
560       while (frame != null) {
561         if (!frame.sourceFile.equals(topFrameLine.sourceFile)) {
562           break;
563         }
564         frames.add(frame);
565         currentLine = reader.readLine();
566         frame = currentLine == null ? null : tryParseFrameLine(currentLine);
567       }
568       retraceStackTrace(defaultRetracer, exceptionLine, frames, tempDir);
569       frames.clear();
570     }
571   }
572 
determineRetracer(String sourceFile, LazyRetracer defaultRetracer)573   private static LazyRetracer determineRetracer(String sourceFile, LazyRetracer defaultRetracer) {
574     LazyRetracer lazyRetracer = getRetracerForR8(sourceFile);
575     if (lazyRetracer != null) {
576       return lazyRetracer;
577     }
578     lazyRetracer = getRetracerForAosp(sourceFile);
579     if (lazyRetracer != null) {
580       return lazyRetracer;
581     }
582     return defaultRetracer;
583   }
584 
retraceStackTrace( LazyRetracer defaultRetracer, ExceptionLine exceptionLine, List<FrameLine> frames, Path tempDir)585   private static void retraceStackTrace(
586       LazyRetracer defaultRetracer,
587       ExceptionLine exceptionLine,
588       List<FrameLine> frames,
589       Path tempDir)
590       throws Exception {
591     String sourceFile = frames.get(0).sourceFile;
592     LazyRetracer lazyRetracer = determineRetracer(sourceFile, defaultRetracer);
593     if (lazyRetracer == null) {
594       printIdentityStackTrace(exceptionLine, frames);
595       return;
596     }
597     Retracer retracer = lazyRetracer.getRetracer(tempDir);
598     List<ResultNode> finalResultNodes = new ArrayList<>();
599     retraceOptionalExceptionLine(
600         retracer,
601         exceptionLine,
602         (context, parentResult) ->
603             retraceFrameRecursive(retracer, context, parentResult, 0, frames)
604                 .forEach(finalResultNodes::add));
605     if (finalResultNodes.isEmpty()) {
606       printIdentityStackTrace(exceptionLine, frames);
607       return;
608     }
609     if (finalResultNodes.size() > 1) {
610       System.out.println(
611           "Printing "
612               + finalResultNodes.size()
613               + " ambiguous stacks separated by <OR>.\n"
614               + "If this is unexpected, please file a bug on R8 and attach the "
615               + "content of the raw stack trace and the mapping file: "
616               + lazyRetracer.getMapLocation()
617               + "\nPublic tracker at https://issuetracker.google.com/issues/new?component=326788");
618     }
619     for (int i = 0; i < finalResultNodes.size(); i++) {
620       if (i > 0) {
621         System.out.println("<OR>");
622       }
623       ResultNode node = finalResultNodes.get(i);
624       node.print();
625     }
626   }
627 
retraceOptionalExceptionLine( Retracer retracer, ExceptionLine exceptionLine, BiConsumer<RetraceStackTraceContext, ResultNode> resultCallback)628   private static void retraceOptionalExceptionLine(
629       Retracer retracer,
630       ExceptionLine exceptionLine,
631       BiConsumer<RetraceStackTraceContext, ResultNode> resultCallback) {
632     // This initial result node parent is 'null', i.e., no parent.
633     ResultNode initialResultNode = null;
634     if (exceptionLine == null) {
635       // If no exception line is given, retracing starts in the empty context.
636       resultCallback.accept(RetraceStackTraceContext.empty(), initialResultNode);
637       return;
638     }
639     // If an exception line is given the result is possibly a forrest, so each individual result
640     // has a null parent.
641     retracer
642         .retraceThrownException(exceptionLine.exception)
643         .forEach(
644             element ->
645                 resultCallback.accept(
646                     element.getContext(),
647                     new ResultNode(
648                         initialResultNode,
649                         exceptionLine.plug(element.getRetracedClass().getTypeName()))));
650   }
651 
retraceFrameRecursive( Retracer retracer, RetraceStackTraceContext context, ResultNode parentResult, int frameIndex, List<FrameLine> frames)652   private static Stream<ResultNode> retraceFrameRecursive(
653       Retracer retracer,
654       RetraceStackTraceContext context,
655       ResultNode parentResult,
656       int frameIndex,
657       List<FrameLine> frames) {
658     if (frameIndex >= frames.size()) {
659       return Stream.of(parentResult);
660     }
661 
662     // Helper to link up frame results when iterating via a closure callback.
663     class ResultLinker {
664       ResultNode current;
665 
666       public ResultLinker(ResultNode current) {
667         this.current = current;
668       }
669 
670       public void link(String nextResult) {
671         current = new ResultNode(current, nextResult);
672       }
673     }
674 
675     FrameLine frameLine = frames.get(frameIndex);
676     return retracer
677         .retraceFrame(context, frameLine.lineNumber, frameLine.clazz, frameLine.methodName)
678         .flatMap(
679             frameElement -> {
680               // Create a linking helper to amend the result when iterating the frames.
681               ResultLinker linker = new ResultLinker(parentResult);
682               frameElement.forEachRewritten(
683                   frame -> {
684                     RetracedMethodReference method = frame.getMethodReference();
685                     RetracedClassReference holder = method.getHolderClass();
686                     int origPos = method.getOriginalPositionOrDefault(-1);
687                     linker.link(
688                         frameLine.plug(
689                             holder.getTypeName()
690                                 + "."
691                                 + method.getMethodName()
692                                 + "("
693                                 + frame.getSourceFile().getOrInferSourceFile()
694                                 + (origPos >= 0 ? (":" + origPos) : "")
695                                 + ")"));
696                   });
697               return retraceFrameRecursive(
698                   retracer,
699                   frameElement.getRetraceStackTraceContext(),
700                   linker.current,
701                   frameIndex + 1,
702                   frames);
703             });
704   }
705 
populateLocalMappingFileMap( List<String> searchPaths, boolean cwdRelativeSearchPaths)706   private static void populateLocalMappingFileMap(
707       List<String> searchPaths, boolean cwdRelativeSearchPaths) throws Exception {
708     Path projectRoot = getProjectRoot();
709     if (projectRoot == null) {
710       return;
711     }
712     Path prebuiltR8MapPath = projectRoot.resolve("prebuilts").resolve("r8").resolve("r8.jar.map");
713     MapInfo prebuiltR8MapInfo = readMapHeaderInfo(prebuiltR8MapPath);
714     if (prebuiltR8MapInfo == null) {
715       info("Unable to read expected prebuilt R8 map in " + prebuiltR8MapPath);
716     } else {
717       RETRACERS.put(
718           prebuiltR8MapInfo.id, new LocalLazyRetracer(prebuiltR8MapInfo, prebuiltR8MapPath));
719     }
720     for (String path : searchPaths) {
721       Path resolvedPath;
722       if (cwdRelativeSearchPaths) {
723         resolvedPath = Paths.get(path);
724       } else {
725         resolvedPath = projectRoot.resolve(Paths.get(path));
726       }
727       if (Files.notExists(resolvedPath)) {
728         error("Invalid search path entry: " + resolvedPath);
729       }
730       Files.walkFileTree(
731           resolvedPath,
732           new FileVisitor<Path>() {
733 
734             final Path mapFileName = Paths.get("proguard_dictionary");
735 
736             @Override
737             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
738               return FileVisitResult.CONTINUE;
739             }
740 
741             @Override
742             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
743                 throws IOException {
744               if (file.endsWith(mapFileName)) {
745                 MapInfo mapInfo = readMapHeaderInfo(file);
746                 if (mapInfo != null) {
747                   RETRACERS.put(mapInfo.id, new LocalLazyRetracer(mapInfo, file));
748                 }
749               }
750               return FileVisitResult.CONTINUE;
751             }
752 
753             @Override
754             public FileVisitResult visitFileFailed(Path file, IOException exc) {
755               return FileVisitResult.CONTINUE;
756             }
757 
758             @Override
759             public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
760               return FileVisitResult.CONTINUE;
761             }
762           });
763     }
764   }
765 
collectMetaMappingFiles(BuildInfo buildInfo, Path directory)766   private static List<Path> collectMetaMappingFiles(BuildInfo buildInfo, Path directory)
767       throws IOException {
768     if (!Files.exists(directory)) {
769       return Collections.emptyList();
770     }
771     String suffix = buildInfo.getMetaMappingFileSuffix();
772     try (Stream<Path> stream = Files.walk(directory)) {
773       return stream.filter(p -> p.toString().endsWith(suffix)).collect(Collectors.toList());
774     }
775   }
776 
populateRemoteMappingFileMap(BuildInfo buildInfo, Path tempDir)777   private static void populateRemoteMappingFileMap(BuildInfo buildInfo, Path tempDir)
778       throws IOException, InterruptedException {
779     Path baseDirectory = getTempBuildDirPath(buildInfo, tempDir);
780     List<Path> metaMappings = collectMetaMappingFiles(buildInfo, baseDirectory);
781     if (metaMappings.isEmpty()) {
782       ensureFetchArtifactCommand();
783       ensureTempBuildDir(buildInfo, tempDir);
784       System.out.println("Fetching meta information for mapping files from build server...");
785       fetchArtifactGlob(buildInfo, "**/*" + buildInfo.getMetaMappingFileSuffix(), baseDirectory);
786       metaMappings = collectMetaMappingFiles(buildInfo, baseDirectory);
787       System.out.println("Meta information files found: " + metaMappings.size());
788       System.out.println();
789     }
790     for (Path metaMapping : metaMappings) {
791       List<String> lines = Files.readAllLines(metaMapping);
792       for (int i = 0; i < lines.size(); i++) {
793         String line = lines.get(i).trim();
794         if (line.startsWith("mappings:") && line.endsWith("{")) {
795           String id = null;
796           String location = null;
797           String type = null;
798           int j = i + 1;
799           while (j < lines.size()) {
800             String subline = lines.get(j++).trim();
801             if (subline.startsWith("}")) {
802               break;
803             }
804             if (subline.startsWith("identifier:")) {
805               id = getQuotedString(subline);
806             } else if (subline.startsWith("location:")) {
807               location = getQuotedString(subline);
808             } else if (subline.startsWith("type:")) {
809               type = subline.substring(5).trim();
810             } else {
811               throw error("no match");
812             }
813           }
814 
815           if (id != null
816               && location != null
817               && type != null
818               && type.equals("R8")
819               && id.length() == 64) {
820             MapInfo mapInfo = new MapInfo(id, id);
821             String mappingFile =
822                 deriveMappingFileFromMetaMapping(buildInfo, baseDirectory, metaMapping);
823             RETRACERS.put(id, new RemoteLazyRetracer(mapInfo, buildInfo, mappingFile, location));
824           } else {
825             List<String> message = new ArrayList<>();
826             message.add("Invalid mapping entry starting at line " + i + ":");
827             for (int k = i; k < j; k++) {
828               message.add(lines.get(k));
829             }
830             throw error(String.join(System.lineSeparator(), message));
831           }
832         }
833       }
834     }
835   }
836 
getQuotedString(String line)837   private static String getQuotedString(String line) {
838     return line.substring(line.indexOf('"') + 1, line.lastIndexOf('"'));
839   }
840 
deriveMappingFileFromMetaMapping( BuildInfo buildInfo, Path base, Path metaMapping)841   private static String deriveMappingFileFromMetaMapping(
842       BuildInfo buildInfo, Path base, Path metaMapping) {
843     String relative = base.relativize(metaMapping).toString();
844     return relative.replace(buildInfo.getMetaMappingFileSuffix(), buildInfo.getMappingFileSuffix());
845   }
846 
readAllLines(InputStream stream)847   private static String readAllLines(InputStream stream) {
848     return new BufferedReader(new InputStreamReader(stream))
849         .lines()
850         .collect(Collectors.joining(System.lineSeparator()));
851   }
852 
ensureFetchArtifactCommand()853   private static void ensureFetchArtifactCommand() {
854     try {
855       Process process = new ProcessBuilder("fetch_artifact", "--help").start();
856       process.destroy();
857     } catch (IOException e) {
858       throw error(
859           String.join(
860               System.lineSeparator(),
861               "Using build identification flags requires 'fetch_artifact'.",
862               "Cannot find 'fetch_artifact' in PATH. Install it using:",
863               "  sudo apt install android-fetch-artifact"));
864     }
865   }
866 
fetchArtifactCommand( BuildInfo buildInfo, String artifact, String entry)867   private static List<String> fetchArtifactCommand(
868       BuildInfo buildInfo, String artifact, String entry) {
869     List<String> command = new ArrayList<>();
870     command.add("fetch_artifact");
871     command.addAll(Arrays.asList("--bid", buildInfo.id));
872     command.addAll(Arrays.asList("--target", buildInfo.target));
873     if (buildInfo.branch != null) {
874       command.addAll(Arrays.asList("--branch", buildInfo.branch));
875     }
876     if (entry != null) {
877       command.addAll(Arrays.asList("--zip_entry", entry));
878     }
879     command.add(artifact);
880     return command;
881   }
882 
fetchArtifact( BuildInfo buildInfo, String artifact, String entry, Path tempDir)883   private static Path fetchArtifact(
884       BuildInfo buildInfo, String artifact, String entry, Path tempDir)
885       throws IOException, InterruptedException {
886     Path tempDirForBuild = ensureTempBuildDir(buildInfo, tempDir);
887     Path outFile = tempDirForBuild.resolve(entry != null ? entry : artifact);
888     if (Files.exists(outFile)) {
889       return outFile;
890     }
891     List<String> command = fetchArtifactCommand(buildInfo, artifact, entry);
892     Process process =
893         new ProcessBuilder(command)
894             .directory(tempDirForBuild.toFile())
895             .redirectError(Redirect.INHERIT)
896             .start();
897     process.waitFor();
898     if (process.exitValue() == 0) {
899       return outFile;
900     }
901     throw error(
902         String.join(
903             System.lineSeparator(),
904             "Failed attempt to fetch_artifact.",
905             "Command: " + String.join(" ", command),
906             "Stdout:",
907             readAllLines(process.getInputStream())));
908   }
909 
fetchArtifactGlob(BuildInfo buildInfo, String artifact, Path tempDirForBuild)910   private static void fetchArtifactGlob(BuildInfo buildInfo, String artifact, Path tempDirForBuild)
911       throws IOException, InterruptedException {
912     List<String> command = fetchArtifactCommand(buildInfo, artifact, null);
913     command.add("--preserve_directory_structure");
914     System.out.println(String.join(" ", command));
915     Process process =
916         new ProcessBuilder(command)
917             .directory(tempDirForBuild.toFile())
918             .redirectError(Redirect.INHERIT)
919             .redirectOutput(Redirect.INHERIT)
920             .start();
921     process.waitFor();
922     if (process.exitValue() != 0) {
923       throw error(
924           String.join(
925               System.lineSeparator(),
926               "Failed attempt to fetch_artifact.",
927               "Command: " + String.join(" ", command)));
928     }
929   }
930 
getTempBuildDirPath(BuildInfo buildInfo, Path tempDir)931   private static Path getTempBuildDirPath(BuildInfo buildInfo, Path tempDir) {
932     return tempDir.resolve(buildInfo.id + "_" + buildInfo.target);
933   }
934 
ensureTempBuildDir(BuildInfo buildInfo, Path tempDir)935   private static Path ensureTempBuildDir(BuildInfo buildInfo, Path tempDir) throws IOException {
936     Path tempDirForBuild = getTempBuildDirPath(buildInfo, tempDir);
937     if (Files.notExists(tempDirForBuild)) {
938       Files.createDirectories(tempDirForBuild);
939     }
940     return tempDirForBuild;
941   }
942 
main(String[] args)943   public static void main(String[] args) throws Exception {
944     String bid = null;
945     String target = null;
946     String branch = null;
947     String stackTraceFile = null;
948     String defaultMapArg = null;
949     boolean printMappingFileTable = false;
950     boolean cwdRelativeSearchPaths = false;
951     Path userTempDir = null;
952     List<String> searchPaths = AOSP_MAP_SEARCH_PATHS;
953     for (int i = 0; i < args.length; i++) {
954       String arg = args[i];
955       if (arg.equals("-h") || arg.equals("--help")) {
956         System.out.println(USAGE);
957         return;
958       }
959       if (arg.equals("--bid")) {
960         i++;
961         if (i == args.length) {
962           throw error("No argument given for --bid");
963         }
964         bid = args[i];
965       } else if (arg.equals("--target")) {
966         i++;
967         if (i == args.length) {
968           throw error("No argument given for --target");
969         }
970         target = args[i];
971       } else if (arg.equals("--branch")) {
972         i++;
973         if (i == args.length) {
974           throw error("No argument given for --branch");
975         }
976         branch = args[i];
977       } else if (arg.equals("--default-map")) {
978         i++;
979         if (i == args.length) {
980           throw error("No argument given for --default-map");
981         }
982         defaultMapArg = args[i];
983       } else if (arg.equals("--map-search-path")) {
984         i++;
985         if (i == args.length) {
986           throw error("No argument given for --map-search-path");
987         }
988         searchPaths = parseSearchPath(args[i]);
989       } else if (arg.equals("--print-map-table")) {
990         printMappingFileTable = true;
991       } else if (arg.equals("--cwd-relative-search-paths")) {
992         cwdRelativeSearchPaths = true;
993       } else if (arg.equals("--temp")) {
994         i++;
995         if (i == args.length) {
996           throw error("No argument given for --temp");
997         }
998         userTempDir = Paths.get(args[i]);
999       } else if (arg.startsWith("-")) {
1000         throw error("Unknown option: " + arg);
1001       } else if (stackTraceFile != null) {
1002         throw error("At most one input file is supported.");
1003       } else {
1004         stackTraceFile = arg;
1005       }
1006     }
1007 
1008     BuildInfo buildInfo = null;
1009     if (bid != null || target != null) {
1010       if (bid == null || target == null) {
1011         throw error("Must supply a target together with a build id.");
1012       }
1013       buildInfo = new BuildInfo(bid, target, branch);
1014     }
1015 
1016     Path tempDir = userTempDir != null ? userTempDir : Files.createTempDirectory("retrace");
1017     try {
1018       if (buildInfo != null) {
1019         populateRemoteMappingFileMap(buildInfo, tempDir);
1020       } else {
1021         populateLocalMappingFileMap(searchPaths, cwdRelativeSearchPaths);
1022       }
1023       if (printMappingFileTable) {
1024         List<String> keys = new ArrayList<>(RETRACERS.keySet());
1025         keys.sort(String::compareTo);
1026         for (String key : keys) {
1027           LazyRetracer retracer = RETRACERS.get(key);
1028           System.out.println(key + " -> " + retracer.getMapLocation());
1029         }
1030         return;
1031       }
1032 
1033       LazyRetracer defaultRetracer = findDefaultRetracer(defaultMapArg);
1034       if (defaultRetracer != null) {
1035         System.out.println("Using default mapping: " + defaultRetracer.getMapLocation());
1036       }
1037 
1038       if (stackTraceFile == null) {
1039         retrace(System.in, defaultRetracer, tempDir);
1040       } else {
1041         Path path = Paths.get(stackTraceFile);
1042         if (!Files.exists(path)) {
1043           throw error("Input file does not exist: " + stackTraceFile);
1044         }
1045         try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) {
1046           retrace(stream, defaultRetracer, tempDir);
1047         }
1048       }
1049       flushPendingMessages();
1050     } finally {
1051       if (userTempDir != tempDir) {
1052         deleteDirectory(tempDir);
1053       }
1054     }
1055   }
1056 
findDefaultRetracer(String key)1057   private static LazyRetracer findDefaultRetracer(String key) {
1058     if (key == null) {
1059       return null;
1060     }
1061     if (Files.isRegularFile(Paths.get(key))) {
1062       return new LocalLazyRetracer(null, Paths.get(key));
1063     }
1064     List<LazyRetracer> matches = new ArrayList<>();
1065     for (LazyRetracer retracer : RETRACERS.values()) {
1066       if (retracer.getMapLocation().contains(key)) {
1067         matches.add(retracer);
1068       }
1069     }
1070     if (matches.size() == 1) {
1071       return matches.get(0);
1072     }
1073     StringBuilder builder = new StringBuilder("--default-map ").append(key);
1074     if (matches.isEmpty()) {
1075       builder
1076           .append(" did not match a local file or any map location in mapping table.")
1077           .append(" (Use --print-map-table to view the table).");
1078     } else {
1079       builder.append(" matched ").append(matches.size()).append(" map paths:\n");
1080       for (LazyRetracer match : matches) {
1081         builder.append(match.getMapLocation()).append('\n');
1082       }
1083     }
1084     throw error(builder.toString());
1085   }
1086 
deleteDirectory(Path directory)1087   private static void deleteDirectory(Path directory) throws IOException {
1088     Files.walk(directory)
1089         .sorted(Comparator.reverseOrder())
1090         .forEachOrdered(
1091             p -> {
1092               try {
1093                 Files.delete(p);
1094               } catch (IOException e) {
1095                 throw new RuntimeException(e);
1096               }
1097             });
1098   }
1099 
parseSearchPath(String paths)1100   private static List<String> parseSearchPath(String paths) {
1101     int length = paths.length();
1102     List<String> result = new ArrayList<>();
1103     int start = 0;
1104     do {
1105       int split = paths.indexOf(':', start);
1106       int end = split != -1 ? split : length;
1107       String path = paths.substring(start, end).trim();
1108       if (!path.isEmpty()) {
1109         result.add(path);
1110       }
1111       start = end + 1;
1112     } while (start < length);
1113     return result;
1114   }
1115 }
1116