175 lines
5.9 KiB
Python
Executable file
175 lines
5.9 KiB
Python
Executable file
#!/usr/bin/env vpython3
|
|
#
|
|
# 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())
|