| #!/usr/bin/env python | 
 | # Copyright (c) 2012 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. | 
 |  | 
 | import hashlib | 
 | import json | 
 | import logging | 
 | import optparse | 
 | import os | 
 | import re | 
 | import shutil | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 |  | 
 |  | 
 | BASE_PATH = os.path.dirname(os.path.abspath(__file__)) | 
 | REDUCE_DEBUGLINE_PATH = os.path.join(BASE_PATH, 'reduce_debugline.py') | 
 | _TOOLS_LINUX_PATH = os.path.join(BASE_PATH, os.pardir, 'linux') | 
 | sys.path.insert(0, _TOOLS_LINUX_PATH) | 
 |  | 
 |  | 
 | from procfs import ProcMaps  # pylint: disable=F0401 | 
 |  | 
 |  | 
 | LOGGER = logging.getLogger('prepare_symbol_info') | 
 |  | 
 |  | 
 | def _dump_command_result(command, output_dir_path, basename, suffix): | 
 |   handle_out, filename_out = tempfile.mkstemp( | 
 |       suffix=suffix, prefix=basename + '.', dir=output_dir_path) | 
 |   handle_err, filename_err = tempfile.mkstemp( | 
 |       suffix=suffix + '.err', prefix=basename + '.', dir=output_dir_path) | 
 |   error = False | 
 |   try: | 
 |     subprocess.check_call( | 
 |         command, stdout=handle_out, stderr=handle_err, shell=True) | 
 |   except (OSError, subprocess.CalledProcessError): | 
 |     error = True | 
 |   finally: | 
 |     os.close(handle_err) | 
 |     os.close(handle_out) | 
 |  | 
 |   if os.path.exists(filename_err): | 
 |     if LOGGER.getEffectiveLevel() <= logging.DEBUG: | 
 |       with open(filename_err, 'r') as f: | 
 |         for line in f: | 
 |           LOGGER.debug(line.rstrip()) | 
 |     os.remove(filename_err) | 
 |  | 
 |   if os.path.exists(filename_out) and ( | 
 |       os.path.getsize(filename_out) == 0 or error): | 
 |     os.remove(filename_out) | 
 |     return None | 
 |  | 
 |   if not os.path.exists(filename_out): | 
 |     return None | 
 |  | 
 |   return filename_out | 
 |  | 
 |  | 
 | def prepare_symbol_info(maps_path, | 
 |                         output_dir_path=None, | 
 |                         alternative_dirs=None, | 
 |                         use_tempdir=False, | 
 |                         use_source_file_name=False): | 
 |   """Prepares (collects) symbol information files for find_runtime_symbols. | 
 |  | 
 |   1) If |output_dir_path| is specified, it tries collecting symbol information | 
 |   files in the given directory |output_dir_path|. | 
 |   1-a) If |output_dir_path| doesn't exist, create the directory and use it. | 
 |   1-b) If |output_dir_path| is an empty directory, use it. | 
 |   1-c) If |output_dir_path| is a directory which has 'files.json', assumes that | 
 |        files are already collected and just ignores it. | 
 |   1-d) Otherwise, depends on |use_tempdir|. | 
 |  | 
 |   2) If |output_dir_path| is not specified, it tries to create a new directory | 
 |   depending on 'maps_path'. | 
 |  | 
 |   If it cannot create a new directory, creates a temporary directory depending | 
 |   on |use_tempdir|.  If |use_tempdir| is False, returns None. | 
 |  | 
 |   Args: | 
 |       maps_path: A path to a file which contains '/proc/<pid>/maps'. | 
 |       alternative_dirs: A mapping from a directory '/path/on/target' where the | 
 |           target process runs to a directory '/path/on/host' where the script | 
 |           reads the binary.  Considered to be used for Android binaries. | 
 |       output_dir_path: A path to a directory where files are prepared. | 
 |       use_tempdir: If True, it creates a temporary directory when it cannot | 
 |           create a new directory. | 
 |       use_source_file_name: If True, it adds reduced result of 'readelf -wL' | 
 |           to find source file names. | 
 |  | 
 |   Returns: | 
 |       A pair of a path to the prepared directory and a boolean representing | 
 |       if it created a temporary directory or not. | 
 |   """ | 
 |   alternative_dirs = alternative_dirs or {} | 
 |   if not output_dir_path: | 
 |     matched = re.match('^(.*)\.maps$', os.path.basename(maps_path)) | 
 |     if matched: | 
 |       output_dir_path = matched.group(1) + '.pre' | 
 |   if not output_dir_path: | 
 |     matched = re.match('^/proc/(.*)/maps$', os.path.realpath(maps_path)) | 
 |     if matched: | 
 |       output_dir_path = matched.group(1) + '.pre' | 
 |   if not output_dir_path: | 
 |     output_dir_path = os.path.basename(maps_path) + '.pre' | 
 |   # TODO(dmikurube): Find another candidate for output_dir_path. | 
 |  | 
 |   used_tempdir = False | 
 |   LOGGER.info('Data for profiling will be collected in "%s".' % output_dir_path) | 
 |   if os.path.exists(output_dir_path): | 
 |     if os.path.isdir(output_dir_path) and not os.listdir(output_dir_path): | 
 |       LOGGER.warn('Using an empty existing directory "%s".' % output_dir_path) | 
 |     else: | 
 |       LOGGER.warn('A file or a directory exists at "%s".' % output_dir_path) | 
 |       if os.path.exists(os.path.join(output_dir_path, 'files.json')): | 
 |         LOGGER.warn('Using the existing directory "%s".' % output_dir_path) | 
 |         return output_dir_path, used_tempdir | 
 |       else: | 
 |         if use_tempdir: | 
 |           output_dir_path = tempfile.mkdtemp() | 
 |           used_tempdir = True | 
 |           LOGGER.warn('Using a temporary directory "%s".' % output_dir_path) | 
 |         else: | 
 |           LOGGER.warn('The directory "%s" is not available.' % output_dir_path) | 
 |           return None, used_tempdir | 
 |   else: | 
 |     LOGGER.info('Creating a new directory "%s".' % output_dir_path) | 
 |     try: | 
 |       os.mkdir(output_dir_path) | 
 |     except OSError: | 
 |       LOGGER.warn('A directory "%s" cannot be created.' % output_dir_path) | 
 |       if use_tempdir: | 
 |         output_dir_path = tempfile.mkdtemp() | 
 |         used_tempdir = True | 
 |         LOGGER.warn('Using a temporary directory "%s".' % output_dir_path) | 
 |       else: | 
 |         LOGGER.warn('The directory "%s" is not available.' % output_dir_path) | 
 |         return None, used_tempdir | 
 |  | 
 |   shutil.copyfile(maps_path, os.path.join(output_dir_path, 'maps')) | 
 |  | 
 |   with open(maps_path, mode='r') as f: | 
 |     maps = ProcMaps.load_file(f) | 
 |  | 
 |   LOGGER.debug('Listing up symbols.') | 
 |   files = {} | 
 |   for entry in maps.iter(ProcMaps.executable): | 
 |     LOGGER.debug('  %016x-%016x +%06x %s' % ( | 
 |         entry.begin, entry.end, entry.offset, entry.name)) | 
 |     binary_path = entry.name | 
 |     for target_path, host_path in alternative_dirs.iteritems(): | 
 |       if entry.name.startswith(target_path): | 
 |         binary_path = entry.name.replace(target_path, host_path, 1) | 
 |     if not (ProcMaps.EXECUTABLE_PATTERN.match(binary_path) or | 
 |             (os.path.isfile(binary_path) and os.access(binary_path, os.X_OK))): | 
 |       continue | 
 |     nm_filename = _dump_command_result( | 
 |         'nm -n --format bsd %s | c++filt' % binary_path, | 
 |         output_dir_path, os.path.basename(binary_path), '.nm') | 
 |     if not nm_filename: | 
 |       continue | 
 |     readelf_e_filename = _dump_command_result( | 
 |         'readelf -eW %s' % binary_path, | 
 |         output_dir_path, os.path.basename(binary_path), '.readelf-e') | 
 |     if not readelf_e_filename: | 
 |       continue | 
 |     readelf_debug_decodedline_file = None | 
 |     if use_source_file_name: | 
 |       readelf_debug_decodedline_file = _dump_command_result( | 
 |           'readelf -wL %s | %s' % (binary_path, REDUCE_DEBUGLINE_PATH), | 
 |           output_dir_path, os.path.basename(binary_path), '.readelf-wL') | 
 |  | 
 |     files[entry.name] = {} | 
 |     files[entry.name]['nm'] = { | 
 |         'file': os.path.basename(nm_filename), | 
 |         'format': 'bsd', | 
 |         'mangled': False} | 
 |     files[entry.name]['readelf-e'] = { | 
 |         'file': os.path.basename(readelf_e_filename)} | 
 |     if readelf_debug_decodedline_file: | 
 |       files[entry.name]['readelf-debug-decodedline-file'] = { | 
 |           'file': os.path.basename(readelf_debug_decodedline_file)} | 
 |  | 
 |     files[entry.name]['size'] = os.stat(binary_path).st_size | 
 |  | 
 |     with open(binary_path, 'rb') as entry_f: | 
 |       md5 = hashlib.md5() | 
 |       sha1 = hashlib.sha1() | 
 |       chunk = entry_f.read(1024 * 1024) | 
 |       while chunk: | 
 |         md5.update(chunk) | 
 |         sha1.update(chunk) | 
 |         chunk = entry_f.read(1024 * 1024) | 
 |       files[entry.name]['sha1'] = sha1.hexdigest() | 
 |       files[entry.name]['md5'] = md5.hexdigest() | 
 |  | 
 |   with open(os.path.join(output_dir_path, 'files.json'), 'w') as f: | 
 |     json.dump(files, f, indent=2, sort_keys=True) | 
 |  | 
 |   LOGGER.info('Collected symbol information at "%s".' % output_dir_path) | 
 |   return output_dir_path, used_tempdir | 
 |  | 
 |  | 
 | def main(): | 
 |   if not sys.platform.startswith('linux'): | 
 |     sys.stderr.write('This script work only on Linux.') | 
 |     return 1 | 
 |  | 
 |   option_parser = optparse.OptionParser( | 
 |       '%s /path/to/maps [/path/to/output_data_dir/]' % sys.argv[0]) | 
 |   option_parser.add_option('--alternative-dirs', dest='alternative_dirs', | 
 |                            metavar='/path/on/target@/path/on/host[:...]', | 
 |                            help='Read files in /path/on/host/ instead of ' | 
 |                            'files in /path/on/target/.') | 
 |   option_parser.add_option('--verbose', dest='verbose', action='store_true', | 
 |                            help='Enable verbose mode.') | 
 |   options, args = option_parser.parse_args(sys.argv) | 
 |   alternative_dirs_dict = {} | 
 |   if options.alternative_dirs: | 
 |     for alternative_dir_pair in options.alternative_dirs.split(':'): | 
 |       target_path, host_path = alternative_dir_pair.split('@', 1) | 
 |       alternative_dirs_dict[target_path] = host_path | 
 |  | 
 |   LOGGER.setLevel(logging.DEBUG) | 
 |   handler = logging.StreamHandler() | 
 |   if options.verbose: | 
 |     handler.setLevel(logging.DEBUG) | 
 |   else: | 
 |     handler.setLevel(logging.INFO) | 
 |   formatter = logging.Formatter('%(message)s') | 
 |   handler.setFormatter(formatter) | 
 |   LOGGER.addHandler(handler) | 
 |  | 
 |   if len(args) < 2: | 
 |     option_parser.error('Argument error.') | 
 |     return 1 | 
 |   elif len(args) == 2: | 
 |     result, _ = prepare_symbol_info(args[1], | 
 |                                     alternative_dirs=alternative_dirs_dict) | 
 |   else: | 
 |     result, _ = prepare_symbol_info(args[1], args[2], | 
 |                                     alternative_dirs=alternative_dirs_dict) | 
 |  | 
 |   return not result | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(main()) |