#!/usr/bin/env python3
#
# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Shutdown adb_logcat_monitor and print accumulated logs.

To test, call './adb_logcat_printer.py <base_dir>' where
<base_dir> contains 'adb logcat -v threadtime' files named as
logcat_<deviceID>_<sequenceNum>

The script will print the files to out, and will combine multiple
logcats from a single device if there is overlap.

Additionally, if a <base_dir>/LOGCAT_MONITOR_PID exists, the script
will attempt to terminate the contained PID by sending a SIGINT and
monitoring for the deletion of the aforementioned file.
"""
# pylint: disable=W0702

import argparse
import io
import logging
import os
import re
import signal
import sys
import time


# Set this to debug for more verbose output
LOG_LEVEL = logging.INFO


def CombineLogFiles(list_of_lists, logger):
    """Splices together multiple logcats from the same device.

    Args:
      list_of_lists: list of pairs (filename, list of timestamped lines)
      logger: handler to log events

    Returns:
      list of lines with duplicates removed
    """
    cur_device_log = ['']
    for cur_file, cur_file_lines in list_of_lists:
        # Ignore files with just the logcat header
        if len(cur_file_lines) < 2:
            continue
        common_index = 0
        # Skip this step if list just has empty string
        if len(cur_device_log) > 1:
            try:
                line = cur_device_log[-1]
                # Used to make sure we only splice on a timestamped line
                if re.match(r'^\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} ', line):
                    common_index = cur_file_lines.index(line)
                else:
                    logger.warning(
                        'splice error - no timestamp in "%s"?', line.strip()
                    )
            except ValueError:
                # The last line was valid but wasn't found in the next file
                cur_device_log += ['***** POSSIBLE INCOMPLETE LOGCAT *****']
                logger.info('Unable to splice %s. Incomplete logcat?', cur_file)

        cur_device_log += ['*' * 30 + '  %s' % cur_file]
        cur_device_log.extend(cur_file_lines[common_index:])

    return cur_device_log


def FindLogFiles(base_dir):
    """Search a directory for logcat files.

    Args:
      base_dir: directory to search

    Returns:
      Mapping of device_id to a sorted list of file paths for a given device
    """
    logcat_filter = re.compile(r'^logcat_(\S+)_(\d+)$')
    # list of tuples (<device_id>, <seq num>, <full file path>)
    filtered_list = []
    for cur_file in os.listdir(base_dir):
        matcher = logcat_filter.match(cur_file)
        if matcher:
            filtered_list += [
                (
                    matcher.group(1),
                    int(matcher.group(2)),
                    os.path.join(base_dir, cur_file),
                )
            ]
    filtered_list.sort()
    file_map = {}
    for device_id, _, cur_file in filtered_list:
        if device_id not in file_map:
            file_map[device_id] = []

        file_map[device_id] += [cur_file]
    return file_map


def GetDeviceLogs(log_filenames, logger):
    """Read log files, combine and format.

    Args:
      log_filenames: mapping of device_id to sorted list of file paths
      logger: logger handle for logging events

    Returns:
      list of formatted device logs, one for each device.
    """
    device_logs = []

    for device, device_files in log_filenames.items():
        logger.debug('%s: %s', device, str(device_files))
        device_file_lines = []
        for cur_file in device_files:
            with open(cur_file, encoding='latin1') as f:
                device_file_lines += [(cur_file, f.read().splitlines())]
        combined_lines = CombineLogFiles(device_file_lines, logger)
        # Prepend each line with a short unique ID so it's easy to see
        # when the device changes.  We don't use the start of the device
        # ID because it can be the same among devices.  Example lines:
        # AB324:  foo
        # AB324:  blah
        device_logs += [('\n' + device[-5:] + ':  ').join(combined_lines)]
    return device_logs


def ShutdownLogcatMonitor(base_dir, logger):
    """Attempts to shutdown adb_logcat_monitor and blocks while waiting."""
    try:
        monitor_pid_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID')
        with open(monitor_pid_path) as f:
            monitor_pid = int(f.readline())

        logger.info('Sending SIGTERM to %d', monitor_pid)
        os.kill(monitor_pid, signal.SIGTERM)
        i = 0
        while True:
            time.sleep(0.2)
            if not os.path.exists(monitor_pid_path):
                return
            if not os.path.exists('/proc/%d' % monitor_pid):
                logger.warning(
                    'Monitor (pid %d) terminated uncleanly?', monitor_pid
                )
                return
            logger.info('Waiting for logcat process to terminate.')
            i += 1
            if i >= 10:
                logger.warning(
                    'Monitor pid did not terminate. Continuing anyway.'
                )
                return

    except (ValueError, IOError, OSError):
        logger.exception('Error signaling logcat monitor - continuing')


def main(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--output-path',
        help='Output file path (if unspecified, prints to stdout)',
    )
    parser.add_argument('log_dir')
    args = parser.parse_args(argv)
    base_dir = args.log_dir

    log_stringio = io.StringIO()
    logger = logging.getLogger('LogcatPrinter')
    logger.setLevel(LOG_LEVEL)
    sh = logging.StreamHandler(log_stringio)
    sh.setFormatter(
        logging.Formatter('%(asctime)-2s %(levelname)-8s %(message)s')
    )
    logger.addHandler(sh)

    if args.output_path:
        if not os.path.exists(os.path.dirname(args.output_path)):
            logger.warning(
                'Output dir %s doesn\'t exist. Creating it.',
                os.path.dirname(args.output_path),
            )
            os.makedirs(os.path.dirname(args.output_path))
        output_file = open(args.output_path, 'w')
        logger.info(
            'Dumping logcat to local file %s. If running in a build, '
            'this file will likely will be uploaded to google storage '
            'in a later step. It can be downloaded from there.',
            args.output_path,
        )
    else:
        output_file = sys.stdout

    try:
        # Wait at least 5 seconds after base_dir is created before printing.
        #
        # The idea is that 'adb logcat > file' output consists of 2 phases:
        #  1 Dump all the saved logs to the file
        #  2 Stream log messages as they are generated
        #
        # We want to give enough time for phase 1 to complete.  There's no
        # good method to tell how long to wait, but it usually only takes a
        # second.  On most bots, this code path won't occur at all, since
        # adb_logcat_monitor.py command will have spawned more than 5 seconds
        # prior to called this shell script.
        try:
            sleep_time = 5 - (time.time() - os.path.getctime(base_dir))
        except OSError:
            sleep_time = 5
        if sleep_time > 0:
            logger.warning('Monitor just started? Sleeping %.1fs', sleep_time)
            time.sleep(sleep_time)

        assert os.path.exists(base_dir), '%s does not exist' % base_dir
        ShutdownLogcatMonitor(base_dir, logger)
        separator = '\n' + '*' * 80 + '\n\n'
        for log in GetDeviceLogs(FindLogFiles(base_dir), logger):
            output_file.write(log)
            output_file.write(separator)
        with open(os.path.join(base_dir, 'eventlog')) as f:
            output_file.write('\nLogcat Monitor Event Log\n')
            output_file.write(f.read())
    except:
        logger.exception('Unexpected exception')

    logger.info('Done.')
    sh.flush()
    output_file.write('\nLogcat Printer Event Log\n')
    output_file.write(log_stringio.getvalue())


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
