1#!/usr/bin/env python 2# 3# Copyright (C) 2017 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Cleans up overlapping portions of traces provided by logcat.""" 18 19from __future__ import absolute_import 20from __future__ import division 21from __future__ import print_function 22 23import argparse 24import re 25import os 26 27STACK_DIVIDER = 65 * "=" 28 29 30def match_to_int(match): 31 """Returns trace line number matches as integers for sorting. 32 Maps other matches to negative integers. 33 """ 34 # Hard coded string are necessary since each trace must have the address 35 # accessed, which is printed before trace lines. 36 if match == "use-after-poison" or match == "unknown-crash": 37 return -2 38 elif match == "READ": 39 return -1 40 # Cutting off non-integer part of match 41 return int(match[1:-1]) 42 43 44def clean_trace_if_valid(trace, stack_min_size, prune_exact): 45 """Cleans trace if it meets a certain standard. Returns None otherwise.""" 46 # Note: Sample input may contain "unknown-crash" instead of 47 # "use-after-poison" 48 # 49 # Sample input: 50 # trace: 51 # "...ERROR: AddressSanitizer: use-after-poison on address 0x0071126a870a... 52 # ...READ of size 2 at 0x0071126a870a thread T0 (droid.deskclock) 53 # ... #0 0x71281013b3 (/data/asan/system/lib64/libart.so+0x2263b3) 54 # ... #1 0x71280fe6b7 (/data/asan/system/lib64/libart.so+0x2236b7) 55 # ... #3 0x71280c22ef (/data/asan/system/lib64/libart.so+0x1e72ef) 56 # ... #2 0x712810a84f (/data/asan/system/lib64/libart.so+0x22f84f)" 57 # 58 # stack_min_size: 2 59 # prune_exact: False 60 # 61 # Sample output: 62 # 63 # "...ERROR: AddressSanitizer: use-after-poison on address 0x0071126a870a... 64 # ...READ of size 2 at 0x0071126a870a thread T0 (droid.deskclock) 65 # ... #0 0x71281013b3 (/data/asan/system/lib64/libart.so+0x2263b3) 66 # ... #1 0x71280fe6b7 (/data/asan/system/lib64/libart.so+0x2236b7) 67 # " 68 69 # Adds a newline character if not present at the end of trace 70 trace = trace if trace[-1] == "\n" else trace + "\n" 71 trace_line_matches = [(match_to_int(match.group()), match.start()) 72 for match in re.finditer("#[0-9]+ " 73 "|use-after-poison" 74 "|unknown-crash" 75 "|READ", trace) 76 ] 77 # Finds the first index where the line number ordering isn't in sequence or 78 # returns the number of matches if it everything is in order. 79 bad_line_no = next((i - 2 for i, match in enumerate(trace_line_matches) 80 if i - 2 != match[0]), len(trace_line_matches) - 2) 81 # If the number ordering breaks after minimum stack size, then the trace is 82 # still valid. 83 if bad_line_no >= stack_min_size: 84 # Added if the trace is already clean 85 trace_line_matches.append((trace_line_matches[-1][0] + 1, len(trace))) 86 bad_match = trace_line_matches[bad_line_no + 2] 87 if prune_exact: 88 bad_match = trace_line_matches[stack_min_size + 2] 89 # Up to new-line that comes before bad line number 90 return trace[:trace.rindex("\n", 0, bad_match[1]) + 1] 91 return None 92 93 94def extant_directory(path_name): 95 """Checks if a path is an actual directory.""" 96 if not os.path.isdir(path_name): 97 dir_error = "%s is not a directory" % (path_name) 98 raise argparse.ArgumentTypeError(dir_error) 99 return path_name 100 101 102def parse_args(): 103 """Parses arguments passed in.""" 104 parser = argparse.ArgumentParser() 105 parser.add_argument("-d", action="store", 106 default="", dest="out_dir_name", type=extant_directory, 107 help="Output Directory") 108 parser.add_argument("-e", action="store_true", 109 default=False, dest="check_exact", 110 help="Forces each trace to be cut to have " 111 "minimum number of lines") 112 parser.add_argument("-m", action="store", 113 default=4, dest="stack_min_size", type=int, 114 help="minimum number of lines a trace should have") 115 parser.add_argument("trace_file", action="store", 116 type=argparse.FileType("r"), 117 help="File only containing lines that are related to " 118 "Sanitizer traces") 119 return parser.parse_args() 120 121 122def main(): 123 """Parses arguments and cleans up traces using other functions.""" 124 stack_min_size = 4 125 check_exact = False 126 127 parsed_argv = parse_args() 128 stack_min_size = parsed_argv.stack_min_size 129 check_exact = parsed_argv.check_exact 130 out_dir_name = parsed_argv.out_dir_name 131 trace_file = parsed_argv.trace_file 132 133 trace_split = trace_file.read().split(STACK_DIVIDER) 134 trace_file.close() 135 trace_clean_split = [clean_trace_if_valid(trace, 136 stack_min_size, 137 check_exact) 138 for trace in trace_split 139 ] 140 trace_clean_split = [trace for trace in trace_clean_split 141 if trace is not None] 142 filename = os.path.basename(trace_file.name + "_filtered") 143 outfile = os.path.join(out_dir_name, filename) 144 with open(outfile, "w") as output_file: 145 output_file.write(STACK_DIVIDER.join(trace_clean_split)) 146 147 filter_percent = 100.0 - (float(len(trace_clean_split)) / 148 len(trace_split) * 100) 149 filter_amount = len(trace_split) - len(trace_clean_split) 150 print("Filtered out %d (%f%%) of %d. %d (%f%%) remain." 151 % (filter_amount, filter_percent, len(trace_split), 152 len(trace_split) - filter_amount, 1 - filter_percent)) 153 154 155if __name__ == "__main__": 156 main() 157