blob: 6750752afd5a50f163a7ba5d7c22a2d3cf653b47 [file] [log] [blame]
Iliyan Malchev4929d6a2011-08-04 17:44:40 -07001#!/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