| # Copyright (c) 2011 The Chromium OS 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 logging |
| import os |
| from autotest_lib.client.common_lib import utils as client_utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import dev_server |
| from autotest_lib.client.cros import constants |
| from autotest_lib.server.cros.dynamic_suite.constants import JOB_BUILD_KEY |
| from autotest_lib.server import utils |
| |
| |
| def generate_minidump_stacktrace(minidump_path): |
| """ |
| Generates a stacktrace for the specified minidump. |
| |
| This function expects the debug symbols to reside under: |
| /build/<board>/usr/lib/debug |
| |
| @param minidump_path: absolute path to minidump to by symbolicated. |
| @raise client_utils.error.CmdError if minidump_stackwalk return code != 0. |
| """ |
| symbol_dir = '%s/../../../lib/debug' % utils.get_server_dir() |
| logging.info('symbol_dir: %s', symbol_dir) |
| client_utils.run('minidump_stackwalk "%s" "%s" > "%s.txt"' % |
| (minidump_path, symbol_dir, minidump_path)) |
| |
| |
| def symbolicate_minidump_with_devserver(minidump_path, resultdir): |
| """ |
| Generates a stack trace for the specified minidump by consulting devserver. |
| |
| This function assumes the debug symbols have been staged on the devserver. |
| |
| @param minidump_path: absolute path to minidump to by symbolicated. |
| @param resultdir: server job's result directory. |
| @raise DevServerException upon failure, HTTP or otherwise. |
| """ |
| # First, look up what build we tested. If we can't find this, we can't |
| # get the right debug symbols, so we might as well give up right now. |
| keyvals = client_utils.read_keyval(resultdir) |
| if JOB_BUILD_KEY not in keyvals: |
| raise dev_server.DevServerException( |
| 'Cannot determine build being tested.') |
| |
| devserver = dev_server.CrashServer.resolve(keyvals[JOB_BUILD_KEY]) |
| trace_text = devserver.symbolicate_dump( |
| minidump_path, keyvals[JOB_BUILD_KEY]) |
| if not trace_text: |
| raise dev_server.DevServerException('Unknown error!!') |
| with open(minidump_path + '.txt', 'w') as trace_file: |
| trace_file.write(trace_text) |
| |
| |
| def find_and_generate_minidump_stacktraces(host_resultdir): |
| """ |
| Finds all minidump files and generates a stack trace for each. |
| |
| Enumerates all files under the test results directory (recursively) |
| and generates a stack trace file for the minidumps. Minidump files are |
| identified as files with .dmp extension. The stack trace filename is |
| composed by appending the .txt extension to the minidump filename. |
| |
| @returns The list of generated minidumps. |
| """ |
| minidumps = [] |
| for dir, subdirs, files in os.walk(host_resultdir): |
| for file in files: |
| if not file.endswith('.dmp'): |
| continue |
| minidump = os.path.join(dir, file) |
| |
| # First, try to symbolicate locally. |
| try: |
| generate_minidump_stacktrace(minidump) |
| logging.info('Generated stack trace for dump %s', minidump) |
| minidumps.append(minidump) |
| continue |
| except client_utils.error.CmdError as err: |
| logging.warn('Failed to generate stack trace locally for ' |
| 'dump %s (rc=%d):\n%r', |
| minidump, err.result_obj.exit_status, err) |
| |
| # If that did not succeed, try to symbolicate using the dev server. |
| try: |
| minidumps.append(minidump) |
| symbolicate_minidump_with_devserver(minidump, host_resultdir) |
| logging.info('Generated stack trace for dump %s', minidump) |
| continue |
| except dev_server.DevServerException as e: |
| logging.warn('Failed to generate stack trace on devserver for ' |
| 'dump %s:\n%r', minidump, e) |
| return minidumps |
| |
| |
| def fetch_orphaned_crashdumps(host, host_resultdir): |
| """ |
| Copy all of the crashes in the crash directory over to the results folder. |
| |
| @param host A host object of the device we're to pull crashes from. |
| @param host_resultdir The result directory for this host for this test run. |
| @return The list of minidumps that we pulled back from the host. |
| """ |
| minidumps = [] |
| for file in host.list_files_glob(os.path.join(constants.CRASH_DIR, '*')): |
| logging.info('Collecting %s...', file) |
| try: |
| host.get_file(file, host_resultdir, preserve_perm=False) |
| minidumps.append(file) |
| except Exception as e: |
| logging.warning('Collection of %s failed:\n%s', file, e) |
| return minidumps |
| |
| |
| def get_site_crashdumps(host, test_start_time): |
| """ |
| Copy all of the crashdumps from a host to the results directory. |
| |
| @param host The host object from which to pull crashes |
| @param test_start_time When the test we just ran started. |
| @return A list of all the minidumps |
| """ |
| host_resultdir = getattr(getattr(host, 'job', None), 'resultdir', None) |
| infodir = os.path.join(host_resultdir, 'crashinfo.%s' % host.hostname) |
| if not os.path.exists(infodir): |
| os.mkdir(infodir) |
| |
| # TODO(milleral): handle orphans differently. crosbug.com/38202 |
| orphans = fetch_orphaned_crashdumps(host, infodir) |
| minidumps = find_and_generate_minidump_stacktraces(host_resultdir) |
| orphans.extend(minidumps) |
| |
| for minidump in orphans: |
| report_bug_from_crash(host, minidump) |
| |
| return orphans |
| |
| |
| def find_packages_of(host, exec_name): |
| """ |
| Find the package that an executable came from. |
| |
| @param host A host object that has the executable. |
| @param exec_name The name of the executable. |
| @return The name of the package that installed the executable. |
| """ |
| packages = [] |
| |
| # TODO(milleral): It would be significantly faster to iterate through |
| # $PATH and run this than to point it at all of / |
| find = host.run('find / -executable -type f -name %s' % exec_name) |
| for full_path in find.stdout.splitlines(): |
| # TODO(milleral): This currently shows scary looking error messages |
| # in the debug logs via stderr. We only look at stdout, so those |
| # get filtered, but it would be good to silence them. |
| portageq = host.run('portageq owners / %s' % full_path) |
| if portageq.stdout: |
| packages.append(portageq.stdout.splitlines()[0].strip()) |
| |
| # TODO(milleral): This chunk of code is here to verify that mapping |
| # executable name to package gives you one and only one package. |
| # It is highly questionable as to if this should be left in the |
| # production version of this code or not. |
| if len(packages) == 0: |
| raise error.NoUniquePackageFound('no package for %s' % exec_name) |
| if len(packages) > 1: |
| # Running through all of /usr/bin in the chroot showed this should |
| # never happen, but still maybe possible? |
| raise error.NoUniquePackageFound('Bug filing found more than one' |
| 'package for %s: %s' % exec_name, packages) |
| |
| # |len(packages) == 1| at this point, as it should be anyway |
| return packages[0] |
| |
| |
| def report_bug_from_crash(host, minidump_path): |
| """ |
| Given a host to query and a minidump, file a bug about the crash. |
| |
| @param host A host object that is where the dump came from |
| @param minidump_path The path to the dump file that should be reported. |
| """ |
| # TODO(milleral): Once this has actually been tested, remove the |
| # try/except. In the meantime, let's make sure nothing dies because of |
| # the fact that this code isn't very heavily tested. |
| try: |
| meta_path = os.path.splitext(minidump_path)[0] + '.meta' |
| with open(meta_path, 'r') as f: |
| for line in f.readlines(): |
| parts = line.split('=') |
| if parts[0] == 'exec_name': |
| packages = find_packages_of(host, parts[1].strip()) |
| logging.info('Would report bug on %s.', packages) |
| break |
| except Exception as e: |
| logging.warning('Bug filing failed with: %s', e) |