The stack tool is not really proprietary, and is needed by vendors and third-party developers working on native code. Change-Id: I37f34b0681a0063ecf71f5a078d2c4a1ba622973 Signed-off-by: Iliyan Malchev <malchev@google.com>
419 lines
14 KiB
Python
Executable File
419 lines
14 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2006 Google Inc. All Rights Reserved.
|
|
|
|
"""stack symbolizes native crash dumps."""
|
|
|
|
import getopt
|
|
import getpass
|
|
import glob
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import urllib
|
|
|
|
import symbol
|
|
|
|
|
|
def PrintUsage():
|
|
"""Print usage and exit with error."""
|
|
# pylint: disable-msg=C6310
|
|
print
|
|
print " usage: " + sys.argv[0] + " [options] [FILE]"
|
|
print
|
|
print " --symbols-dir=path"
|
|
print " the path to a symbols dir, such as =/tmp/out/target/product/dream/symbols"
|
|
print
|
|
print " --symbols-zip=path"
|
|
print " the path to a symbols zip file, such as =dream-symbols-12345.zip"
|
|
print
|
|
print " --auto"
|
|
print " attempt to:"
|
|
print " 1) automatically find the build number in the crash"
|
|
print " 2) if it's an official build, download the symbols "
|
|
print " from the build server, and use them"
|
|
print
|
|
print " FILE should contain a stack trace in it somewhere"
|
|
print " the tool will find that and re-print it with"
|
|
print " source files and line numbers. If you don't"
|
|
print " pass FILE, or if file is -, it reads from"
|
|
print " stdin."
|
|
print
|
|
# pylint: enable-msg=C6310
|
|
sys.exit(1)
|
|
|
|
|
|
class SSOCookie(object):
|
|
"""Creates a cookie file so we can download files from the build server."""
|
|
|
|
def __init__(self, cookiename=".sso.cookie", keep=False):
|
|
self.sso_server = "login.corp.google.com"
|
|
self.name = cookiename
|
|
self.keeper = keep
|
|
if not os.path.exists(self.name):
|
|
user = os.environ["USER"]
|
|
print "\n%s, to access the symbols, please enter your LDAP " % user,
|
|
sys.stdout.flush()
|
|
password = getpass.getpass()
|
|
params = urllib.urlencode({"u": user, "pw": password})
|
|
url = "https://%s/login?ssoformat=CORP_SSO" % self.sso_server
|
|
# login to SSO
|
|
curlcmd = ["/usr/bin/curl",
|
|
"--cookie", self.name,
|
|
"--cookie-jar", self.name,
|
|
"--silent",
|
|
"--location",
|
|
"--data", params,
|
|
"--output", "/dev/null",
|
|
url]
|
|
subprocess.check_call(curlcmd)
|
|
if os.path.exists(self.name):
|
|
os.chmod(self.name, 0600)
|
|
else:
|
|
print "Could not log in to SSO"
|
|
sys.exit(1)
|
|
|
|
def __del__(self):
|
|
"""Clean up."""
|
|
if not self.keeper:
|
|
os.remove(self.name)
|
|
|
|
|
|
class NoBuildIDException(Exception):
|
|
pass
|
|
|
|
|
|
def FindBuildFingerprint(lines):
|
|
"""Searches the given file (array of lines) for the build fingerprint."""
|
|
fingerprint_regex = re.compile("^.*Build fingerprint:\s'(?P<fingerprint>.*)'")
|
|
for line in lines:
|
|
fingerprint_search = fingerprint_regex.match(line.strip())
|
|
if fingerprint_search:
|
|
return fingerprint_search.group("fingerprint")
|
|
|
|
return None # didn't find the fingerprint string, so return none
|
|
|
|
|
|
class SymbolDownloadException(Exception):
|
|
pass
|
|
|
|
|
|
DEFAULT_SYMROOT = "/tmp/symbols"
|
|
|
|
|
|
def DownloadSymbols(fingerprint, cookie):
|
|
"""Attempts to download the symbols from the build server.
|
|
|
|
If successful, extracts them, and returns the path.
|
|
|
|
Args:
|
|
fingerprint: build fingerprint from the input stack trace
|
|
cookie: SSOCookie
|
|
|
|
Returns:
|
|
tuple (None, None) if no fingerprint is provided. Otherwise
|
|
tuple (root directory, symbols directory).
|
|
|
|
Raises:
|
|
SymbolDownloadException: Problem downloading symbols for fingerprint
|
|
"""
|
|
if fingerprint is None:
|
|
return (None, None)
|
|
symdir = "%s/%s" % (DEFAULT_SYMROOT, hash(fingerprint))
|
|
if not os.path.exists(symdir):
|
|
os.makedirs(symdir)
|
|
# build server figures out the branch based on the CL
|
|
params = {
|
|
"op": "GET-SYMBOLS-LINK",
|
|
"fingerprint": fingerprint,
|
|
}
|
|
print "url: http://android-build/buildbot-update?" + urllib.urlencode(params)
|
|
url = urllib.urlopen("http://android-build/buildbot-update?",
|
|
urllib.urlencode(params)).readlines()[0]
|
|
if not url:
|
|
raise SymbolDownloadException("Build server down? Failed to find syms...")
|
|
|
|
regex_str = (r"(?P<base_url>http\:\/\/android-build\/builds\/.*\/[0-9]+)"
|
|
r"(?P<img>.*)")
|
|
url_regex = re.compile(regex_str)
|
|
url_match = url_regex.match(url)
|
|
if url_match is None:
|
|
raise SymbolDownloadException("Unexpected results from build server URL...")
|
|
|
|
base_url = url_match.group("base_url")
|
|
img = url_match.group("img")
|
|
symbolfile = img.replace("-img-", "-symbols-")
|
|
symurl = base_url + symbolfile
|
|
localsyms = symdir + symbolfile
|
|
|
|
if not os.path.exists(localsyms):
|
|
print "downloading %s ..." % symurl
|
|
curlcmd = ["/usr/bin/curl",
|
|
"--cookie", cookie.name,
|
|
"--silent",
|
|
"--location",
|
|
"--write-out", "%{http_code}",
|
|
"--output", localsyms,
|
|
symurl]
|
|
p = subprocess.Popen(curlcmd,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
close_fds=True)
|
|
code = p.stdout.read()
|
|
err = p.stderr.read()
|
|
if err:
|
|
raise SymbolDownloadException("stderr from curl download: %s" % err)
|
|
if code != "200":
|
|
raise SymbolDownloadException("Faied to download %s" % symurl)
|
|
else:
|
|
print "using existing cache for symbols"
|
|
|
|
return UnzipSymbols(localsyms, symdir)
|
|
|
|
|
|
def UnzipSymbols(symbolfile, symdir=None):
|
|
"""Unzips a file to DEFAULT_SYMROOT and returns the unzipped location.
|
|
|
|
Args:
|
|
symbolfile: The .zip file to unzip
|
|
symdir: Optional temporary directory to use for extraction
|
|
|
|
Returns:
|
|
A tuple containing (the directory into which the zip file was unzipped,
|
|
the path to the "symbols" directory in the unzipped file). To clean
|
|
up, the caller can delete the first element of the tuple.
|
|
|
|
Raises:
|
|
SymbolDownloadException: When the unzip fails.
|
|
"""
|
|
if not symdir:
|
|
symdir = "%s/%s" % (DEFAULT_SYMROOT, hash(symbolfile))
|
|
if not os.path.exists(symdir):
|
|
os.makedirs(symdir)
|
|
|
|
print "extracting %s..." % symbolfile
|
|
saveddir = os.getcwd()
|
|
os.chdir(symdir)
|
|
try:
|
|
unzipcode = subprocess.call(["unzip", "-qq", "-o", symbolfile])
|
|
if unzipcode > 0:
|
|
os.remove(symbolfile)
|
|
raise SymbolDownloadException("failed to extract symbol files (%s)."
|
|
% symbolfile)
|
|
finally:
|
|
os.chdir(saveddir)
|
|
|
|
return (symdir, glob.glob("%s/out/target/product/*/symbols" % symdir)[0])
|
|
|
|
|
|
def PrintTraceLines(trace_lines):
|
|
"""Print back trace."""
|
|
maxlen = max(map(lambda tl: len(tl[1]), trace_lines))
|
|
print
|
|
print "Stack Trace:"
|
|
print " RELADDR " + "FUNCTION".ljust(maxlen) + " FILE:LINE"
|
|
for tl in trace_lines:
|
|
(addr, symbol_with_offset, location) = tl
|
|
print " %8s %s %s" % (addr, symbol_with_offset.ljust(maxlen), location)
|
|
return
|
|
|
|
|
|
def PrintValueLines(value_lines):
|
|
"""Print stack data values."""
|
|
print
|
|
print "Stack Data:"
|
|
print " ADDR VALUE FILE:LINE/FUNCTION"
|
|
for vl in value_lines:
|
|
(addr, value, symbol_with_offset, location) = vl
|
|
print " " + addr + " " + value + " " + location
|
|
if location:
|
|
print " " + symbol_with_offset
|
|
return
|
|
|
|
UNKNOWN = "<unknown>"
|
|
HEAP = "[heap]"
|
|
STACK = "[stack]"
|
|
|
|
|
|
def ConvertTrace(lines):
|
|
"""Convert strings containing native crash to a stack."""
|
|
process_info_line = re.compile("(pid: [0-9]+, tid: [0-9]+.*)")
|
|
signal_line = re.compile("(signal [0-9]+ \(.*\).*)")
|
|
register_line = re.compile("(([ ]*[0-9a-z]{2} [0-9a-f]{8}){4})")
|
|
thread_line = re.compile("(.*)(\-\-\- ){15}\-\-\-")
|
|
# Note taht both trace and value line matching allow for variable amounts of
|
|
# whitespace (e.g. \t). This is because the we want to allow for the stack
|
|
# tool to operate on AndroidFeedback provided system logs. AndroidFeedback
|
|
# strips out double spaces that are found in tombsone files and logcat output.
|
|
#
|
|
# Examples of matched trace lines include lines from tombstone files like:
|
|
# #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so
|
|
# Or lines from AndroidFeedback crash report system logs like:
|
|
# 03-25 00:51:05.520 I/DEBUG ( 65): #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so
|
|
# Please note the spacing differences.
|
|
trace_line = re.compile("(.*)\#([0-9]+)[ \t]+(..)[ \t]+([0-9a-f]{8})[ \t]+([^\r\n \t]*)( \((.*)\))?") # pylint: disable-msg=C6310
|
|
# Examples of matched value lines include:
|
|
# bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so
|
|
# 03-25 00:51:05.530 I/DEBUG ( 65): bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so
|
|
# Again, note the spacing differences.
|
|
value_line = re.compile("(.*)([0-9a-f]{8})[ \t]+([0-9a-f]{8})[ \t]+([^\r\n \t]*)")
|
|
# Lines from 'code around' sections of the output will be matched before
|
|
# value lines because otheriwse the 'code around' sections will be confused as
|
|
# value lines.
|
|
#
|
|
# Examples include:
|
|
# 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8
|
|
# 03-25 00:51:05.530 I/DEBUG ( 65): 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8
|
|
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
|
|
|
|
trace_lines = []
|
|
value_lines = []
|
|
|
|
for ln in lines:
|
|
# AndroidFeedback adds zero width spaces into its crash reports. These
|
|
# should be removed or the regular expresssions will fail to match.
|
|
line = unicode(ln, errors='ignore')
|
|
header = process_info_line.search(line)
|
|
if header:
|
|
print header.group(1)
|
|
continue
|
|
header = signal_line.search(line)
|
|
if header:
|
|
print header.group(1)
|
|
continue
|
|
header = register_line.search(line)
|
|
if header:
|
|
print header.group(1)
|
|
continue
|
|
if trace_line.match(line):
|
|
match = trace_line.match(line)
|
|
(unused_0, unused_1, unused_2,
|
|
code_addr, area, symbol_present, symbol_name) = match.groups()
|
|
|
|
if area == UNKNOWN or area == HEAP or area == STACK:
|
|
trace_lines.append((code_addr, area, area))
|
|
else:
|
|
# If a calls b which further calls c and c is inlined to b, we want to
|
|
# display "a -> b -> c" in the stack trace instead of just "a -> c"
|
|
(source_symbol,
|
|
source_location,
|
|
object_symbol_with_offset) = symbol.SymbolInformation(area, code_addr)
|
|
if not source_symbol:
|
|
if symbol_present:
|
|
source_symbol = symbol.CallCppFilt(symbol_name)
|
|
else:
|
|
source_symbol = UNKNOWN
|
|
if not source_location:
|
|
source_location = area
|
|
if not object_symbol_with_offset:
|
|
object_symbol_with_offset = source_symbol
|
|
if not object_symbol_with_offset.startswith(source_symbol):
|
|
trace_lines.append(("v------>", source_symbol, source_location))
|
|
trace_lines.append((code_addr,
|
|
object_symbol_with_offset,
|
|
source_location))
|
|
else:
|
|
trace_lines.append((code_addr,
|
|
object_symbol_with_offset,
|
|
source_location))
|
|
if code_line.match(line):
|
|
# Code lines should be ignored. If this were exluded the 'code around'
|
|
# sections would trigger value_line matches.
|
|
continue;
|
|
if value_line.match(line):
|
|
match = value_line.match(line)
|
|
(unused_, addr, value, area) = match.groups()
|
|
if area == UNKNOWN or area == HEAP or area == STACK or not area:
|
|
value_lines.append((addr, value, area, ""))
|
|
else:
|
|
(source_symbol,
|
|
source_location,
|
|
object_symbol_with_offset) = symbol.SymbolInformation(area, value)
|
|
if not source_location:
|
|
source_location = ""
|
|
if not object_symbol_with_offset:
|
|
object_symbol_with_offset = UNKNOWN
|
|
value_lines.append((addr,
|
|
value,
|
|
object_symbol_with_offset,
|
|
source_location))
|
|
header = thread_line.search(line)
|
|
if header:
|
|
if trace_lines:
|
|
PrintTraceLines(trace_lines)
|
|
|
|
if value_lines:
|
|
PrintValueLines(value_lines)
|
|
trace_lines = []
|
|
value_lines = []
|
|
print
|
|
print "-----------------------------------------------------\n"
|
|
|
|
if trace_lines:
|
|
PrintTraceLines(trace_lines)
|
|
|
|
if value_lines:
|
|
PrintValueLines(value_lines)
|
|
|
|
|
|
def main():
|
|
try:
|
|
options, arguments = getopt.getopt(sys.argv[1:], "",
|
|
["auto",
|
|
"symbols-dir=",
|
|
"symbols-zip=",
|
|
"help"])
|
|
except getopt.GetoptError, unused_error:
|
|
PrintUsage()
|
|
|
|
zip_arg = None
|
|
auto = False
|
|
fingerprint = None
|
|
for option, value in options:
|
|
if option == "--help":
|
|
PrintUsage()
|
|
elif option == "--symbols-dir":
|
|
symbol.SYMBOLS_DIR = os.path.expanduser(value)
|
|
elif option == "--symbols-zip":
|
|
zip_arg = os.path.expanduser(value)
|
|
elif option == "--auto":
|
|
auto = True
|
|
|
|
if len(arguments) > 1:
|
|
PrintUsage()
|
|
|
|
if auto:
|
|
cookie = SSOCookie(".symbols.cookie")
|
|
|
|
if not arguments or arguments[0] == "-":
|
|
print "Reading native crash info from stdin"
|
|
f = sys.stdin
|
|
else:
|
|
print "Searching for native crashes in %s" % arguments[0]
|
|
f = open(arguments[0], "r")
|
|
|
|
lines = f.readlines()
|
|
f.close()
|
|
|
|
rootdir = None
|
|
if auto:
|
|
fingerprint = FindBuildFingerprint(lines)
|
|
print "fingerprint:", fingerprint
|
|
rootdir, symbol.SYMBOLS_DIR = DownloadSymbols(fingerprint, cookie)
|
|
elif zip_arg:
|
|
rootdir, symbol.SYMBOLS_DIR = UnzipSymbols(zip_arg)
|
|
|
|
print "Reading symbols from", symbol.SYMBOLS_DIR
|
|
ConvertTrace(lines)
|
|
|
|
if rootdir:
|
|
# be a good citizen and clean up...os.rmdir and os.removedirs() don't work
|
|
cmd = "rm -rf \"%s\"" % rootdir
|
|
print "\ncleaning up (%s)" % cmd
|
|
os.system(cmd)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
# vi: ts=2 sw=2
|