| #!/usr/bin/env vpython | |
| # | |
| # Copyright 2019 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. | |
| # Fetches Crashpad dumps from a given device, walks and symbolizes the stacks. | |
| # All the non-trivial operations are performed by generate_breakpad_symbols.py, | |
| # dump_syms, minidump_dump and minidump_stackwalk. | |
| import argparse | |
| import logging | |
| import os | |
| import posixpath | |
| import re | |
| import sys | |
| import shutil | |
| import subprocess | |
| import tempfile | |
| _BUILD_ANDROID_PATH = os.path.abspath( | |
| os.path.join(os.path.dirname(__file__), '..')) | |
| sys.path.append(_BUILD_ANDROID_PATH) | |
| import devil_chromium | |
| from devil.android import device_utils | |
| from devil.utils import timeout_retry | |
| def _CreateSymbolsDir(build_path, dynamic_library_names): | |
| generator = os.path.normpath( | |
| os.path.join(_BUILD_ANDROID_PATH, '..', '..', 'components', 'crash', | |
| 'content', 'tools', 'generate_breakpad_symbols.py')) | |
| syms_dir = os.path.join(build_path, 'crashpad_syms') | |
| shutil.rmtree(syms_dir, ignore_errors=True) | |
| os.mkdir(syms_dir) | |
| for lib in dynamic_library_names: | |
| unstripped_library_path = os.path.join(build_path, 'lib.unstripped', lib) | |
| if not os.path.exists(unstripped_library_path): | |
| continue | |
| logging.info('Generating symbols for: %s', unstripped_library_path) | |
| cmd = [ | |
| generator, | |
| '--symbols-dir', | |
| syms_dir, | |
| '--build-dir', | |
| build_path, | |
| '--binary', | |
| unstripped_library_path, | |
| '--platform', | |
| 'android', | |
| ] | |
| return_code = subprocess.call(cmd) | |
| if return_code != 0: | |
| logging.error('Could not extract symbols, command failed: %s', | |
| ' '.join(cmd)) | |
| return syms_dir | |
| def _ChooseLatestCrashpadDump(device, crashpad_dump_path): | |
| if not device.PathExists(crashpad_dump_path): | |
| logging.warning('Crashpad dump directory does not exist: %s', | |
| crashpad_dump_path) | |
| return None | |
| latest = None | |
| latest_timestamp = 0 | |
| for crashpad_file in device.ListDirectory(crashpad_dump_path): | |
| if crashpad_file.endswith('.dmp'): | |
| stat = device.StatPath(posixpath.join(crashpad_dump_path, crashpad_file)) | |
| current_timestamp = stat['st_mtime'] | |
| if current_timestamp > latest_timestamp: | |
| latest_timestamp = current_timestamp | |
| latest = crashpad_file | |
| return latest | |
| def _ExtractLibraryNamesFromDump(build_path, dump_path): | |
| default_library_name = 'libmonochrome.so' | |
| dumper_path = os.path.join(build_path, 'minidump_dump') | |
| if not os.access(dumper_path, os.X_OK): | |
| logging.warning( | |
| 'Cannot extract library name from dump because %s is not found, ' | |
| 'default to: %s', dumper_path, default_library_name) | |
| return [default_library_name] | |
| p = subprocess.Popen([dumper_path, dump_path], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| stdout, stderr = p.communicate() | |
| if p.returncode != 0: | |
| # Dumper errors often do not affect stack walkability, just a warning. | |
| logging.warning('Reading minidump failed with output:\n%s', stderr) | |
| library_names = [] | |
| module_library_line_re = re.compile(r'[(]code_file[)]\s+= ' | |
| r'"(?P<library_name>lib[^. ]+.so)"') | |
| in_module = False | |
| for line in stdout.splitlines(): | |
| line = line.lstrip().rstrip('\n') | |
| if line == 'MDRawModule': | |
| in_module = True | |
| continue | |
| if line == '': | |
| in_module = False | |
| continue | |
| if in_module: | |
| m = module_library_line_re.match(line) | |
| if m: | |
| library_names.append(m.group('library_name')) | |
| if not library_names: | |
| logging.warning( | |
| 'Could not find any library name in the dump, ' | |
| 'default to: %s', default_library_name) | |
| return [default_library_name] | |
| return library_names | |
| def main(): | |
| logging.basicConfig(level=logging.INFO) | |
| parser = argparse.ArgumentParser( | |
| description='Fetches Crashpad dumps from a given device, ' | |
| 'walks and symbolizes the stacks.') | |
| parser.add_argument('--device', required=True, help='Device serial number') | |
| parser.add_argument('--adb-path', help='Path to the "adb" command') | |
| parser.add_argument( | |
| '--build-path', | |
| required=True, | |
| help='Build output directory, equivalent to CHROMIUM_OUTPUT_DIR') | |
| parser.add_argument( | |
| '--chrome-cache-path', | |
| required=True, | |
| help='Directory on the device where Chrome stores cached files,' | |
| ' crashpad stores dumps in a subdirectory of it') | |
| args = parser.parse_args() | |
| stackwalk_path = os.path.join(args.build_path, 'minidump_stackwalk') | |
| if not os.path.exists(stackwalk_path): | |
| logging.error('Missing minidump_stackwalk executable') | |
| return 1 | |
| devil_chromium.Initialize(output_directory=args.build_path, | |
| adb_path=args.adb_path) | |
| device = device_utils.DeviceUtils(args.device) | |
| device_crashpad_path = posixpath.join(args.chrome_cache_path, 'Crashpad', | |
| 'pending') | |
| def CrashpadDumpExists(): | |
| return _ChooseLatestCrashpadDump(device, device_crashpad_path) | |
| crashpad_file = timeout_retry.WaitFor( | |
| CrashpadDumpExists, wait_period=1, max_tries=9) | |
| if not crashpad_file: | |
| logging.error('Could not locate a crashpad dump') | |
| return 1 | |
| dump_dir = tempfile.mkdtemp() | |
| symbols_dir = None | |
| try: | |
| device.PullFile( | |
| device_path=posixpath.join(device_crashpad_path, crashpad_file), | |
| host_path=dump_dir) | |
| dump_full_path = os.path.join(dump_dir, crashpad_file) | |
| library_names = _ExtractLibraryNamesFromDump(args.build_path, | |
| dump_full_path) | |
| symbols_dir = _CreateSymbolsDir(args.build_path, library_names) | |
| stackwalk_cmd = [stackwalk_path, dump_full_path, symbols_dir] | |
| subprocess.call(stackwalk_cmd) | |
| finally: | |
| shutil.rmtree(dump_dir, ignore_errors=True) | |
| if symbols_dir: | |
| shutil.rmtree(symbols_dir, ignore_errors=True) | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(main()) |