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