#!/usr/bin/env vpython3
#
# Copyright 2013 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.
#
# Find the most recent tombstone file(s) on all connected devices
# and prints their stacks.
#
# Assumes tombstone file was created with current symbols.

import argparse
import datetime
import logging
import os
import sys

from multiprocessing.pool import ThreadPool

import devil_chromium

from devil.android import device_denylist
from devil.android import device_errors
from devil.android import device_utils
from devil.utils import run_tests_helper
from pylib import constants
from pylib.symbols import stack_symbolizer


_TZ_UTC = {'TZ': 'UTC'}


def _ListTombstones(device):
  """List the tombstone files on the device.

  Args:
    device: An instance of DeviceUtils.

  Yields:
    Tuples of (tombstone filename, date time of file on device).
  """
  try:
    if not device.PathExists('/data/tombstones', as_root=True):
      return
    entries = device.StatDirectory('/data/tombstones', as_root=True)
    for entry in entries:
      if 'tombstone' in entry['filename']:
        yield (entry['filename'],
               datetime.datetime.fromtimestamp(entry['st_mtime']))
  except device_errors.CommandFailedError:
    logging.exception('Could not retrieve tombstones.')
  except device_errors.DeviceUnreachableError:
    logging.exception('Device unreachable retrieving tombstones.')
  except device_errors.CommandTimeoutError:
    logging.exception('Timed out retrieving tombstones.')


def _GetDeviceDateTime(device):
  """Determine the date time on the device.

  Args:
    device: An instance of DeviceUtils.

  Returns:
    A datetime instance.
  """
  device_now_string = device.RunShellCommand(
      ['date'], check_return=True, env=_TZ_UTC)
  return datetime.datetime.strptime(
      device_now_string[0], '%a %b %d %H:%M:%S %Z %Y')


def _GetTombstoneData(device, tombstone_file):
  """Retrieve the tombstone data from the device

  Args:
    device: An instance of DeviceUtils.
    tombstone_file: the tombstone to retrieve

  Returns:
    A list of lines
  """
  return device.ReadFile(
      '/data/tombstones/' + tombstone_file, as_root=True).splitlines()


def _EraseTombstone(device, tombstone_file):
  """Deletes a tombstone from the device.

  Args:
    device: An instance of DeviceUtils.
    tombstone_file: the tombstone to delete.
  """
  return device.RunShellCommand(
      ['rm', '/data/tombstones/' + tombstone_file],
      as_root=True, check_return=True)


def _ResolveTombstone(args):
  tombstone = args[0]
  tombstone_symbolizer = args[1]
  lines = []
  lines += [tombstone['file'] + ' created on ' + str(tombstone['time']) +
            ', about this long ago: ' +
            (str(tombstone['device_now'] - tombstone['time']) +
            ' Device: ' + tombstone['serial'])]
  logging.info('\n'.join(lines))
  logging.info('Resolving...')
  lines += tombstone_symbolizer.ExtractAndResolveNativeStackTraces(
      tombstone['data'],
      tombstone['device_abi'],
      tombstone['stack'])
  return lines


def _ResolveTombstones(jobs, tombstones, tombstone_symbolizer):
  """Resolve a list of tombstones.

  Args:
    jobs: the number of jobs to use with multithread.
    tombstones: a list of tombstones.
  """
  if not tombstones:
    logging.warning('No tombstones to resolve.')
    return []
  if len(tombstones) == 1:
    data = [_ResolveTombstone([tombstones[0], tombstone_symbolizer])]
  else:
    pool = ThreadPool(jobs)
    data = pool.map(
        _ResolveTombstone,
        [[tombstone, tombstone_symbolizer] for tombstone in tombstones])
    pool.close()
    pool.join()
  resolved_tombstones = []
  for tombstone in data:
    resolved_tombstones.extend(tombstone)
  return resolved_tombstones


def _GetTombstonesForDevice(device, resolve_all_tombstones,
                            include_stack_symbols,
                            wipe_tombstones):
  """Returns a list of tombstones on a given device.

  Args:
    device: An instance of DeviceUtils.
    resolve_all_tombstone: Whether to resolve every tombstone.
    include_stack_symbols: Whether to include symbols for stack data.
    wipe_tombstones: Whether to wipe tombstones.
  """
  ret = []
  all_tombstones = list(_ListTombstones(device))
  if not all_tombstones:
    logging.warning('No tombstones.')
    return ret

  # Sort the tombstones in date order, descending
  all_tombstones.sort(key=lambda a: a[1], reverse=True)

  # Only resolve the most recent unless --all-tombstones given.
  tombstones = all_tombstones if resolve_all_tombstones else [all_tombstones[0]]

  device_now = _GetDeviceDateTime(device)
  try:
    for tombstone_file, tombstone_time in tombstones:
      ret += [{'serial': str(device),
               'device_abi': device.product_cpu_abi,
               'device_now': device_now,
               'time': tombstone_time,
               'file': tombstone_file,
               'stack': include_stack_symbols,
               'data': _GetTombstoneData(device, tombstone_file)}]
  except device_errors.CommandFailedError:
    for entry in device.StatDirectory(
        '/data/tombstones', as_root=True, timeout=60):
      logging.info('%s: %s', str(device), entry)
    raise

  # Erase all the tombstones if desired.
  if wipe_tombstones:
    for tombstone_file, _ in all_tombstones:
      _EraseTombstone(device, tombstone_file)

  return ret


def ClearAllTombstones(device):
  """Clear all tombstones in the device.

  Args:
    device: An instance of DeviceUtils.
  """
  all_tombstones = list(_ListTombstones(device))
  if not all_tombstones:
    logging.warning('No tombstones to clear.')

  for tombstone_file, _ in all_tombstones:
    _EraseTombstone(device, tombstone_file)


def ResolveTombstones(device, resolve_all_tombstones, include_stack_symbols,
                      wipe_tombstones, jobs=4, apk_under_test=None,
                      tombstone_symbolizer=None):
  """Resolve tombstones in the device.

  Args:
    device: An instance of DeviceUtils.
    resolve_all_tombstone: Whether to resolve every tombstone.
    include_stack_symbols: Whether to include symbols for stack data.
    wipe_tombstones: Whether to wipe tombstones.
    jobs: Number of jobs to use when processing multiple crash stacks.

  Returns:
    A list of resolved tombstones.
  """
  return _ResolveTombstones(jobs,
                            _GetTombstonesForDevice(device,
                                                    resolve_all_tombstones,
                                                    include_stack_symbols,
                                                    wipe_tombstones),
                            (tombstone_symbolizer
                             or stack_symbolizer.Symbolizer(apk_under_test)))


def main():
  custom_handler = logging.StreamHandler(sys.stdout)
  custom_handler.setFormatter(run_tests_helper.CustomFormatter())
  logging.getLogger().addHandler(custom_handler)
  logging.getLogger().setLevel(logging.INFO)

  parser = argparse.ArgumentParser()
  parser.add_argument('--device',
                      help='The serial number of the device. If not specified '
                           'will use all devices.')
  parser.add_argument('--denylist-file', help='Device denylist JSON file.')
  parser.add_argument('-a', '--all-tombstones', action='store_true',
                      help='Resolve symbols for all tombstones, rather than '
                           'just the most recent.')
  parser.add_argument('-s', '--stack', action='store_true',
                      help='Also include symbols for stack data')
  parser.add_argument('-w', '--wipe-tombstones', action='store_true',
                      help='Erase all tombstones from device after processing')
  parser.add_argument('-j', '--jobs', type=int,
                      default=4,
                      help='Number of jobs to use when processing multiple '
                           'crash stacks.')
  parser.add_argument('--output-directory',
                      help='Path to the root build directory.')
  parser.add_argument('--adb-path', type=os.path.abspath,
                      help='Path to the adb binary.')
  args = parser.parse_args()

  if args.output_directory:
    constants.SetOutputDirectory(args.output_directory)

  devil_chromium.Initialize(output_directory=constants.GetOutDirectory(),
                            adb_path=args.adb_path)

  denylist = (device_denylist.Denylist(args.denylist_file)
              if args.denylist_file else None)

  if args.device:
    devices = [device_utils.DeviceUtils(args.device)]
  else:
    devices = device_utils.DeviceUtils.HealthyDevices(denylist)

  # This must be done serially because strptime can hit a race condition if
  # used for the first time in a multithreaded environment.
  # http://bugs.python.org/issue7980
  for device in devices:
    resolved_tombstones = ResolveTombstones(
        device, args.all_tombstones,
        args.stack, args.wipe_tombstones, args.jobs)
    for line in resolved_tombstones:
      logging.info(line)


if __name__ == '__main__':
  sys.exit(main())
