Home | History | Annotate | Download | only in scripts
      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
    419