Iliyan Malchev | 4929d6a | 2011-08-04 17:44:40 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2006 Google Inc. All Rights Reserved. |
| 4 | |
| 5 | """stack symbolizes native crash dumps.""" |
| 6 | |
| 7 | import getopt |
| 8 | import getpass |
| 9 | import glob |
| 10 | import os |
| 11 | import re |
| 12 | import subprocess |
| 13 | import sys |
| 14 | import urllib |
| 15 | |
| 16 | import symbol |
| 17 | |
| 18 | |
| 19 | def PrintUsage(): |
| 20 | """Print usage and exit with error.""" |
| 21 | # pylint: disable-msg=C6310 |
| 22 | print |
| 23 | print " usage: " + sys.argv[0] + " [options] [FILE]" |
| 24 | print |
| 25 | print " --symbols-dir=path" |
| 26 | print " the path to a symbols dir, such as =/tmp/out/target/product/dream/symbols" |
| 27 | print |
| 28 | print " --symbols-zip=path" |
| 29 | print " the path to a symbols zip file, such as =dream-symbols-12345.zip" |
| 30 | print |
| 31 | print " --auto" |
| 32 | print " attempt to:" |
| 33 | print " 1) automatically find the build number in the crash" |
| 34 | print " 2) if it's an official build, download the symbols " |
| 35 | print " from the build server, and use them" |
| 36 | print |
| 37 | print " FILE should contain a stack trace in it somewhere" |
| 38 | print " the tool will find that and re-print it with" |
| 39 | print " source files and line numbers. If you don't" |
| 40 | print " pass FILE, or if file is -, it reads from" |
| 41 | print " stdin." |
| 42 | print |
| 43 | # pylint: enable-msg=C6310 |
| 44 | sys.exit(1) |
| 45 | |
| 46 | |
| 47 | class SSOCookie(object): |
| 48 | """Creates a cookie file so we can download files from the build server.""" |
| 49 | |
| 50 | def __init__(self, cookiename=".sso.cookie", keep=False): |
| 51 | self.sso_server = "login.corp.google.com" |
| 52 | self.name = cookiename |
| 53 | self.keeper = keep |
| 54 | if not os.path.exists(self.name): |
| 55 | user = os.environ["USER"] |
| 56 | print "\n%s, to access the symbols, please enter your LDAP " % user, |
| 57 | sys.stdout.flush() |
| 58 | password = getpass.getpass() |
| 59 | params = urllib.urlencode({"u": user, "pw": password}) |
| 60 | url = "https://%s/login?ssoformat=CORP_SSO" % self.sso_server |
| 61 | # login to SSO |
| 62 | curlcmd = ["/usr/bin/curl", |
| 63 | "--cookie", self.name, |
| 64 | "--cookie-jar", self.name, |
| 65 | "--silent", |
| 66 | "--location", |
| 67 | "--data", params, |
| 68 | "--output", "/dev/null", |
| 69 | url] |
| 70 | subprocess.check_call(curlcmd) |
| 71 | if os.path.exists(self.name): |
| 72 | os.chmod(self.name, 0600) |
| 73 | else: |
| 74 | print "Could not log in to SSO" |
| 75 | sys.exit(1) |
| 76 | |
| 77 | def __del__(self): |
| 78 | """Clean up.""" |
| 79 | if not self.keeper: |
| 80 | os.remove(self.name) |
| 81 | |
| 82 | |
| 83 | class NoBuildIDException(Exception): |
| 84 | pass |
| 85 | |
| 86 | |
| 87 | def FindBuildFingerprint(lines): |
| 88 | """Searches the given file (array of lines) for the build fingerprint.""" |
| 89 | fingerprint_regex = re.compile("^.*Build fingerprint:\s'(?P<fingerprint>.*)'") |
| 90 | for line in lines: |
| 91 | fingerprint_search = fingerprint_regex.match(line.strip()) |
| 92 | if fingerprint_search: |
| 93 | return fingerprint_search.group("fingerprint") |
| 94 | |
| 95 | return None # didn't find the fingerprint string, so return none |
| 96 | |
| 97 | |
| 98 | class SymbolDownloadException(Exception): |
| 99 | pass |
| 100 | |
| 101 | |
| 102 | DEFAULT_SYMROOT = "/tmp/symbols" |
| 103 | |
| 104 | |
| 105 | def DownloadSymbols(fingerprint, cookie): |
| 106 | """Attempts to download the symbols from the build server. |
| 107 | |
| 108 | If successful, extracts them, and returns the path. |
| 109 | |
| 110 | Args: |
| 111 | fingerprint: build fingerprint from the input stack trace |
| 112 | cookie: SSOCookie |
| 113 | |
| 114 | Returns: |
| 115 | tuple (None, None) if no fingerprint is provided. Otherwise |
| 116 | tuple (root directory, symbols directory). |
| 117 | |
| 118 | Raises: |
| 119 | SymbolDownloadException: Problem downloading symbols for fingerprint |
| 120 | """ |
| 121 | if fingerprint is None: |
| 122 | return (None, None) |
| 123 | symdir = "%s/%s" % (DEFAULT_SYMROOT, hash(fingerprint)) |
| 124 | if not os.path.exists(symdir): |
| 125 | os.makedirs(symdir) |
| 126 | # build server figures out the branch based on the CL |
| 127 | params = { |
| 128 | "op": "GET-SYMBOLS-LINK", |
| 129 | "fingerprint": fingerprint, |
| 130 | } |
| 131 | print "url: http://android-build/buildbot-update?" + urllib.urlencode(params) |
| 132 | url = urllib.urlopen("http://android-build/buildbot-update?", |
| 133 | urllib.urlencode(params)).readlines()[0] |
| 134 | if not url: |
| 135 | raise SymbolDownloadException("Build server down? Failed to find syms...") |
| 136 | |
| 137 | regex_str = (r"(?P<base_url>http\:\/\/android-build\/builds\/.*\/[0-9]+)" |
| 138 | r"(?P<img>.*)") |
| 139 | url_regex = re.compile(regex_str) |
| 140 | url_match = url_regex.match(url) |
| 141 | if url_match is None: |
| 142 | raise SymbolDownloadException("Unexpected results from build server URL...") |
| 143 | |
| 144 | base_url = url_match.group("base_url") |
| 145 | img = url_match.group("img") |
| 146 | symbolfile = img.replace("-img-", "-symbols-") |
| 147 | symurl = base_url + symbolfile |
| 148 | localsyms = symdir + symbolfile |
| 149 | |
| 150 | if not os.path.exists(localsyms): |
| 151 | print "downloading %s ..." % symurl |
| 152 | curlcmd = ["/usr/bin/curl", |
| 153 | "--cookie", cookie.name, |
| 154 | "--silent", |
| 155 | "--location", |
| 156 | "--write-out", "%{http_code}", |
| 157 | "--output", localsyms, |
| 158 | symurl] |
| 159 | p = subprocess.Popen(curlcmd, |
| 160 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 161 | close_fds=True) |
| 162 | code = p.stdout.read() |
| 163 | err = p.stderr.read() |
| 164 | if err: |
| 165 | raise SymbolDownloadException("stderr from curl download: %s" % err) |
| 166 | if code != "200": |
| 167 | raise SymbolDownloadException("Faied to download %s" % symurl) |
| 168 | else: |
| 169 | print "using existing cache for symbols" |
| 170 | |
| 171 | return UnzipSymbols(localsyms, symdir) |
| 172 | |
| 173 | |
| 174 | def UnzipSymbols(symbolfile, symdir=None): |
| 175 | """Unzips a file to DEFAULT_SYMROOT and returns the unzipped location. |
| 176 | |
| 177 | Args: |
| 178 | symbolfile: The .zip file to unzip |
| 179 | symdir: Optional temporary directory to use for extraction |
| 180 | |
| 181 | Returns: |
| 182 | A tuple containing (the directory into which the zip file was unzipped, |
| 183 | the path to the "symbols" directory in the unzipped file). To clean |
| 184 | up, the caller can delete the first element of the tuple. |
| 185 | |
| 186 | Raises: |
| 187 | SymbolDownloadException: When the unzip fails. |
| 188 | """ |
| 189 | if not symdir: |
| 190 | symdir = "%s/%s" % (DEFAULT_SYMROOT, hash(symbolfile)) |
| 191 | if not os.path.exists(symdir): |
| 192 | os.makedirs(symdir) |
| 193 | |
| 194 | print "extracting %s..." % symbolfile |
| 195 | saveddir = os.getcwd() |
| 196 | os.chdir(symdir) |
| 197 | try: |
| 198 | unzipcode = subprocess.call(["unzip", "-qq", "-o", symbolfile]) |
| 199 | if unzipcode > 0: |
| 200 | os.remove(symbolfile) |
| 201 | raise SymbolDownloadException("failed to extract symbol files (%s)." |
| 202 | % symbolfile) |
| 203 | finally: |
| 204 | os.chdir(saveddir) |
| 205 | |
| 206 | return (symdir, glob.glob("%s/out/target/product/*/symbols" % symdir)[0]) |
| 207 | |
| 208 | |
| 209 | def PrintTraceLines(trace_lines): |
| 210 | """Print back trace.""" |
| 211 | maxlen = max(map(lambda tl: len(tl[1]), trace_lines)) |
| 212 | print |
| 213 | print "Stack Trace:" |
| 214 | print " RELADDR " + "FUNCTION".ljust(maxlen) + " FILE:LINE" |
| 215 | for tl in trace_lines: |
| 216 | (addr, symbol_with_offset, location) = tl |
| 217 | print " %8s %s %s" % (addr, symbol_with_offset.ljust(maxlen), location) |
| 218 | return |
| 219 | |
| 220 | |
| 221 | def PrintValueLines(value_lines): |
| 222 | """Print stack data values.""" |
| 223 | print |
| 224 | print "Stack Data:" |
| 225 | print " ADDR VALUE FILE:LINE/FUNCTION" |
| 226 | for vl in value_lines: |
| 227 | (addr, value, symbol_with_offset, location) = vl |
| 228 | print " " + addr + " " + value + " " + location |
| 229 | if location: |
| 230 | print " " + symbol_with_offset |
| 231 | return |
| 232 | |
| 233 | UNKNOWN = "<unknown>" |
| 234 | HEAP = "[heap]" |
| 235 | STACK = "[stack]" |
| 236 | |
| 237 | |
| 238 | def ConvertTrace(lines): |
| 239 | """Convert strings containing native crash to a stack.""" |
| 240 | process_info_line = re.compile("(pid: [0-9]+, tid: [0-9]+.*)") |
| 241 | signal_line = re.compile("(signal [0-9]+ \(.*\).*)") |
| 242 | register_line = re.compile("(([ ]*[0-9a-z]{2} [0-9a-f]{8}){4})") |
| 243 | thread_line = re.compile("(.*)(\-\-\- ){15}\-\-\-") |
| 244 | # Note taht both trace and value line matching allow for variable amounts of |
| 245 | # whitespace (e.g. \t). This is because the we want to allow for the stack |
| 246 | # tool to operate on AndroidFeedback provided system logs. AndroidFeedback |
| 247 | # strips out double spaces that are found in tombsone files and logcat output. |
| 248 | # |
| 249 | # Examples of matched trace lines include lines from tombstone files like: |
| 250 | # #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so |
| 251 | # Or lines from AndroidFeedback crash report system logs like: |
| 252 | # 03-25 00:51:05.520 I/DEBUG ( 65): #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so |
| 253 | # Please note the spacing differences. |
| 254 | trace_line = re.compile("(.*)\#([0-9]+)[ \t]+(..)[ \t]+([0-9a-f]{8})[ \t]+([^\r\n \t]*)( \((.*)\))?") # pylint: disable-msg=C6310 |
| 255 | # Examples of matched value lines include: |
| 256 | # bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so |
| 257 | # 03-25 00:51:05.530 I/DEBUG ( 65): bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so |
| 258 | # Again, note the spacing differences. |
| 259 | value_line = re.compile("(.*)([0-9a-f]{8})[ \t]+([0-9a-f]{8})[ \t]+([^\r\n \t]*)") |
| 260 | # Lines from 'code around' sections of the output will be matched before |
| 261 | # value lines because otheriwse the 'code around' sections will be confused as |
| 262 | # value lines. |
| 263 | # |
| 264 | # Examples include: |
| 265 | # 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8 |
| 266 | # 03-25 00:51:05.530 I/DEBUG ( 65): 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8 |
| 267 | code_line = re.compile("(.*)[ \t]*[a-f0-9]{8}[ \t]*[a-f0-9]{8}[ \t]*[a-f0-9]{8}[ \t]*[a-f0-9]{8}[ \t]*[a-f0-9]{8}[ \t]*[ \r\n]") # pylint: disable-msg=C6310 |
| 268 | |
| 269 | trace_lines = [] |
| 270 | value_lines = [] |
| 271 | |
| 272 | for ln in lines: |
| 273 | # AndroidFeedback adds zero width spaces into its crash reports. These |
| 274 | # should be removed or the regular expresssions will fail to match. |
| 275 | line = unicode(ln, errors='ignore') |
| 276 | header = process_info_line.search(line) |
| 277 | if header: |
| 278 | print header.group(1) |
| 279 | continue |
| 280 | header = signal_line.search(line) |
| 281 | if header: |
| 282 | print header.group(1) |
| 283 | continue |
| 284 | header = register_line.search(line) |
| 285 | if header: |
| 286 | print header.group(1) |
| 287 | continue |
| 288 | if trace_line.match(line): |
| 289 | match = trace_line.match(line) |
| 290 | (unused_0, unused_1, unused_2, |
| 291 | code_addr, area, symbol_present, symbol_name) = match.groups() |
| 292 | |
| 293 | if area == UNKNOWN or area == HEAP or area == STACK: |
| 294 | trace_lines.append((code_addr, area, area)) |
| 295 | else: |
| 296 | # If a calls b which further calls c and c is inlined to b, we want to |
| 297 | # display "a -> b -> c" in the stack trace instead of just "a -> c" |
| 298 | (source_symbol, |
| 299 | source_location, |
| 300 | object_symbol_with_offset) = symbol.SymbolInformation(area, code_addr) |
| 301 | if not source_symbol: |
| 302 | if symbol_present: |
| 303 | source_symbol = symbol.CallCppFilt(symbol_name) |
| 304 | else: |
| 305 | source_symbol = UNKNOWN |
| 306 | if not source_location: |
| 307 | source_location = area |
| 308 | if not object_symbol_with_offset: |
| 309 | object_symbol_with_offset = source_symbol |
| 310 | if not object_symbol_with_offset.startswith(source_symbol): |
| 311 | trace_lines.append(("v------>", source_symbol, source_location)) |
| 312 | trace_lines.append((code_addr, |
| 313 | object_symbol_with_offset, |
| 314 | source_location)) |
| 315 | else: |
| 316 | trace_lines.append((code_addr, |
| 317 | object_symbol_with_offset, |
| 318 | source_location)) |
| 319 | if code_line.match(line): |
| 320 | # Code lines should be ignored. If this were exluded the 'code around' |
| 321 | # sections would trigger value_line matches. |
| 322 | continue; |
| 323 | if value_line.match(line): |
| 324 | match = value_line.match(line) |
| 325 | (unused_, addr, value, area) = match.groups() |
| 326 | if area == UNKNOWN or area == HEAP or area == STACK or not area: |
| 327 | value_lines.append((addr, value, area, "")) |
| 328 | else: |
| 329 | (source_symbol, |
| 330 | source_location, |
| 331 | object_symbol_with_offset) = symbol.SymbolInformation(area, value) |
| 332 | if not source_location: |
| 333 | source_location = "" |
| 334 | if not object_symbol_with_offset: |
| 335 | object_symbol_with_offset = UNKNOWN |
| 336 | value_lines.append((addr, |
| 337 | value, |
| 338 | object_symbol_with_offset, |
| 339 | source_location)) |
| 340 | header = thread_line.search(line) |
| 341 | if header: |
| 342 | if trace_lines: |
| 343 | PrintTraceLines(trace_lines) |
| 344 | |
| 345 | if value_lines: |
| 346 | PrintValueLines(value_lines) |
| 347 | trace_lines = [] |
| 348 | value_lines = [] |
| 349 | print |
| 350 | print "-----------------------------------------------------\n" |
| 351 | |
| 352 | if trace_lines: |
| 353 | PrintTraceLines(trace_lines) |
| 354 | |
| 355 | if value_lines: |
| 356 | PrintValueLines(value_lines) |
| 357 | |
| 358 | |
| 359 | def main(): |
| 360 | try: |
| 361 | options, arguments = getopt.getopt(sys.argv[1:], "", |
| 362 | ["auto", |
| 363 | "symbols-dir=", |
| 364 | "symbols-zip=", |
| 365 | "help"]) |
| 366 | except getopt.GetoptError, unused_error: |
| 367 | PrintUsage() |
| 368 | |
| 369 | zip_arg = None |
| 370 | auto = False |
| 371 | fingerprint = None |
| 372 | for option, value in options: |
| 373 | if option == "--help": |
| 374 | PrintUsage() |
| 375 | elif option == "--symbols-dir": |
| 376 | symbol.SYMBOLS_DIR = os.path.expanduser(value) |
| 377 | elif option == "--symbols-zip": |
| 378 | zip_arg = os.path.expanduser(value) |
| 379 | elif option == "--auto": |
| 380 | auto = True |
| 381 | |
| 382 | if len(arguments) > 1: |
| 383 | PrintUsage() |
| 384 | |
| 385 | if auto: |
| 386 | cookie = SSOCookie(".symbols.cookie") |
| 387 | |
| 388 | if not arguments or arguments[0] == "-": |
| 389 | print "Reading native crash info from stdin" |
| 390 | f = sys.stdin |
| 391 | else: |
| 392 | print "Searching for native crashes in %s" % arguments[0] |
| 393 | f = open(arguments[0], "r") |
| 394 | |
| 395 | lines = f.readlines() |
| 396 | f.close() |
| 397 | |
| 398 | rootdir = None |
| 399 | if auto: |
| 400 | fingerprint = FindBuildFingerprint(lines) |
| 401 | print "fingerprint:", fingerprint |
| 402 | rootdir, symbol.SYMBOLS_DIR = DownloadSymbols(fingerprint, cookie) |
| 403 | elif zip_arg: |
| 404 | rootdir, symbol.SYMBOLS_DIR = UnzipSymbols(zip_arg) |
| 405 | |
| 406 | print "Reading symbols from", symbol.SYMBOLS_DIR |
| 407 | ConvertTrace(lines) |
| 408 | |
| 409 | if rootdir: |
| 410 | # be a good citizen and clean up...os.rmdir and os.removedirs() don't work |
| 411 | cmd = "rm -rf \"%s\"" % rootdir |
| 412 | print "\ncleaning up (%s)" % cmd |
| 413 | os.system(cmd) |
| 414 | |
| 415 | if __name__ == "__main__": |
| 416 | main() |
| 417 | |
| 418 | # vi: ts=2 sw=2 |