• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2006 Google Inc. All Rights Reserved.
4
5"""stack symbolizes native crash dumps."""
6
7import getopt
8import getpass
9import glob
10import os
11import re
12import subprocess
13import sys
14import urllib
15
16import symbol
17
18
19def 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
47class 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
83class NoBuildIDException(Exception):
84  pass
85
86
87def 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
98class SymbolDownloadException(Exception):
99  pass
100
101
102DEFAULT_SYMROOT = "/tmp/symbols"
103
104
105def 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
174def 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
209def 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
221def 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
233UNKNOWN = "<unknown>"
234HEAP = "[heap]"
235STACK = "[stack]"
236
237
238def 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
359def 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
415if __name__ == "__main__":
416  main()
417
418# vi: ts=2 sw=2
419