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