Improve android stack parser There was several issues with the Android stack parser that prevented it to work correctly in a number of cases. The two cases fixed here: - When a mojo app is retrieved with arguments (dart_content_handler.mojo?strict=true), we want to ignore ?strict=true when looking for a match on the local filesystem. - There were cases when paths were not normalized correctly (for example, double-slashes), preventing the libraries to match. This fixes domokit/devtools#23 R=ppi@chromium.org, ppi Review URL: https://codereview.chromium.org/1306603002 . Cr-Mirrored-From: https://github.com/domokit/mojo Cr-Mirrored-Commit: 763f44f16c1f564be73b9c6dbaef3edba542b157
diff --git a/android_stack_parser/stack b/android_stack_parser/stack index 97913ab..f75d097 100755 --- a/android_stack_parser/stack +++ b/android_stack_parser/stack
@@ -17,11 +17,11 @@ """stack symbolizes native crash dumps.""" import getopt -import glob import os -import re +import os.path import sys +import stack_utils import stack_core import subprocess import symbol @@ -71,102 +71,6 @@ # pylint: enable-msg=C6310 sys.exit(1) -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) - - android_symbols = glob.glob("%s/out/target/product/*/symbols" % symdir) - if android_symbols: - return (symdir, android_symbols[0]) - else: - # This is a zip of Chrome symbols, so symbol.CHROME_SYMBOLS_DIR needs to be - # updated to point here. - symbol.CHROME_SYMBOLS_DIR = symdir - return (symdir, symdir) - - -def GetBasenameFromMojoApp(url): - """Used by GetSymbolMapping() to extract the basename from the location the - mojo app was downloaded from. The location is a URL, e.g. - http://foo/bar/x.so.""" - index = url.rfind('/') - return url[(index + 1):] if index != -1 else url - - -def GetSymboledNameForMojoApp(path): - """Used by GetSymbolMapping to get the non-stripped library name for an - installed mojo app.""" - # e.g. tracing.mojo -> libtracing_library.so - name, ext = os.path.splitext(path) - if ext != '.mojo': - return path - return 'lib%s_library.so' % name - - -def GetSymbolMapping(lines): - """Returns a mapping (dictionary) from download file to .so.""" - regex = re.compile('Caching mojo app (\S+) at (\S+)') - mappings = {} - for line in lines: - result = regex.search(line) - if result: - url = GetBasenameFromMojoApp(result.group(1)) - mappings[result.group(2)] = GetSymboledNameForMojoApp(url) - return mappings - - -def _LowestAncestorContainingRelpath(dir_path, relpath): - """Returns the lowest ancestor dir of |dir_path| that contains |relpath|. - """ - cur_dir_path = os.path.abspath(dir_path) - while True: - if os.path.exists(os.path.join(cur_dir_path, relpath)): - return cur_dir_path - - next_dir_path = os.path.dirname(cur_dir_path) - if next_dir_path != cur_dir_path: - cur_dir_path = next_dir_path - else: - return None - - -def _GuessDir(relpath): - """Returns absolute path to location |relpath| in the lowest ancestor of this - file that contains it.""" - lowest_ancestor = _LowestAncestorContainingRelpath( - os.path.dirname(__file__), relpath) - if not lowest_ancestor: - return None - return os.path.join(lowest_ancestor, relpath) - def main(): @@ -204,7 +108,7 @@ more_info = False if not symbol.BUILD_DIR: - guess = _GuessDir(_DEFAULT_BUILD_DIR) + guess = stack_utils.GuessDir(_DEFAULT_BUILD_DIR) if not guess: print "Couldn't find the build directory, please pass --build-dir." return 1 @@ -212,7 +116,7 @@ symbol.BUILD_DIR = guess if not symbol.NDK_DIR: - guess = _GuessDir(_DEFAULT_NDK_DIR) + guess = stack_utils.GuessDir(_DEFAULT_NDK_DIR) if not guess: print "Couldn't find the Android NDK, please pass --ndk-dir." return 1 @@ -235,13 +139,13 @@ rootdir = None if zip_arg: - rootdir, symbol.SYMBOLS_DIR = UnzipSymbols(zip_arg) + rootdir, symbol.SYMBOLS_DIR = stack_utils.UnzipSymbols(zip_arg) if symbol.SYMBOLS_DIR: print "Reading Android symbols from", symbol.SYMBOLS_DIR print "Reading Mojo symbols from", symbol.BUILD_DIR - stack_core.ConvertTrace(lines, more_info, GetSymbolMapping(lines)) + stack_core.ConvertTrace(lines, more_info, stack_utils.GetSymbolMapping(lines)) if rootdir: # be a good citizen and clean up...os.rmdir and os.removedirs() don't work
diff --git a/android_stack_parser/stack_utils.py b/android_stack_parser/stack_utils.py new file mode 100644 index 0000000..f6efb86 --- /dev/null +++ b/android_stack_parser/stack_utils.py
@@ -0,0 +1,108 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Utility functions for the stack tool.""" + +import glob +import os +import os.path +import re + + +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) + + android_symbols = glob.glob("%s/out/target/product/*/symbols" % symdir) + if android_symbols: + return (symdir, android_symbols[0]) + else: + # This is a zip of Chrome symbols, so symbol.CHROME_SYMBOLS_DIR needs to be + # updated to point here. + symbol.CHROME_SYMBOLS_DIR = symdir + return (symdir, symdir) + + +def GetBasenameFromMojoApp(url): + """Used by GetSymbolMapping() to extract the basename from the location the + mojo app was downloaded from. The location is a URL, e.g. + http://foo/bar/x.so.""" + index = url.rfind('/') + return url[(index + 1):] if index != -1 else url + + +def GetSymboledNameForMojoApp(path): + """Used by GetSymbolMapping to get the non-stripped library name for an + installed mojo app.""" + # e.g. tracing.mojo -> libtracing_library.so + name, ext = os.path.splitext(path) + if ext != '.mojo': + return path + return 'lib%s_library.so' % name + + +def GetSymbolMapping(lines): + """Returns a mapping (dictionary) from download file to .so.""" + regex = re.compile('Caching mojo app (\S+?)(?:\?\S+)? at (\S+)') + mappings = {} + for line in lines: + result = regex.search(line) + if result: + url = GetBasenameFromMojoApp(result.group(1)) + mappings[os.path.normpath(result.group(2))] = GetSymboledNameForMojoApp( + url) + return mappings + + +def _LowestAncestorContainingRelpath(dir_path, relpath): + """Returns the lowest ancestor dir of |dir_path| that contains |relpath|. + """ + cur_dir_path = os.path.abspath(dir_path) + while True: + if os.path.exists(os.path.join(cur_dir_path, relpath)): + return cur_dir_path + + next_dir_path = os.path.dirname(cur_dir_path) + if next_dir_path != cur_dir_path: + cur_dir_path = next_dir_path + else: + return None + + +def GuessDir(relpath): + """Returns absolute path to location |relpath| in the lowest ancestor of this + file that contains it.""" + lowest_ancestor = _LowestAncestorContainingRelpath( + os.path.dirname(__file__), relpath) + if not lowest_ancestor: + return None + return os.path.join(lowest_ancestor, relpath)
diff --git a/android_stack_parser/stack_utils_unittest.py b/android_stack_parser/stack_utils_unittest.py new file mode 100644 index 0000000..6329aa6 --- /dev/null +++ b/android_stack_parser/stack_utils_unittest.py
@@ -0,0 +1,70 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for the native crash dump symbolizer utility functions.""" + +import unittest +import stack_utils + + +class StackUtilsTest(unittest.TestCase): + """Tests the native crash dump symbolizer utility functions.""" + + def test_GetSymbolMapping_no_match(self): + """Verifies that, if no mapping is on the input, no mapping is on the + output. + """ + lines = ["This is a test case\n", "Caching all mojo apps", ""] + self.assertDictEqual({}, stack_utils.GetSymbolMapping(lines)) + + def test_GetSymbolMapping_simple_match(self): + """Verifies a simple symbol mapping.""" + lines = ["This is a test case\n", "Caching all mojo apps", + "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app " + "https://apps.mojo/myapp.mojo at /path/to/myapp.mojo/.lM03ws"] + golden_dict = { + "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so" + } + actual_dict = stack_utils.GetSymbolMapping(lines) + self.assertDictEqual(golden_dict, actual_dict) + + def test_GetSymbolMapping_multiple_match(self): + """Verifies mapping of multiple symbol files.""" + lines = ["This is a test case\n", "Caching all mojo apps", + "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app " + "https://apps.mojo/myapp.mojo at /path/to/myapp.mojo/.lM03ws", + "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app " + "https://apps.mojo/otherapp.mojo at /path/to/otherapp.mojo/.kW07s"] + golden_dict = { + "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so", + "/path/to/otherapp.mojo/.kW07s": "libotherapp_library.so" + } + actual_dict = stack_utils.GetSymbolMapping(lines) + self.assertDictEqual(golden_dict, actual_dict) + + def test_GetSymbolMapping_parameter_match(self): + """Verifies parameters are stripped from mappings.""" + lines = ["This is a test case\n", "Caching all mojo apps", + "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app " + "https://apps.mojo/myapp.mojo?q=hello at /path/to/myapp.mojo/.lM03ws"] + golden_dict = { + "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so" + } + actual_dict = stack_utils.GetSymbolMapping(lines) + self.assertDictEqual(golden_dict, actual_dict) + + def test_GetSymbolMapping_normalize(self): + """Verifies paths are normalized in mappings.""" + lines = ["This is a test case\n", "Caching all mojo apps", + "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app " + "https://apps.mojo/myapp.mojo at /path/to/.//myapp.mojo/.lM03ws"] + golden_dict = { + "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so" + } + actual_dict = stack_utils.GetSymbolMapping(lines) + self.assertDictEqual(golden_dict, actual_dict) + + +if __name__ == "__main__": + unittest.main()