#!/usr/bin/env python
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Symbolizes stack traces from logcat.
See https://developer.android.com/ndk/guides/ndk-stack for more information.
"""

from __future__ import print_function

import argparse
import os
import re
import subprocess
import sys


def find_llvm_symbolizer():
    """Finds the NDK llvm-symbolizer(1) binary.

    Returns: An absolute path to llvm-symbolizer(1)."""

    # ndk-stack is installed to $NDK/prebuilt/<platform>/bin, so from
    # `~/Downloads/android-ndk-r18/prebuilt/linux-x86_64/bin/ndk-stack`...
    # ...get `/usr/enh/Downloads/android-ndk-r18/`:
    ndk_bin = os.path.dirname(os.path.realpath(__file__))
    ndk_root = os.path.abspath(os.path.join(ndk_bin, '../../..'))
    # ...get `linux-x86_64`:
    arch = os.path.basename(os.path.abspath(os.path.join(ndk_bin, '../')))
    # And from there construct the llvm-symbolizer path.
    llvm_bin = os.path.join(ndk_root, 'toolchains', 'llvm', 'prebuilt', arch,
                            'bin')
    return os.path.join(llvm_bin, 'llvm-symbolizer')


def main():
    """"Program entry point."""
    parser = argparse.ArgumentParser(
        description='Symbolizes Android crashes.',
        epilog='See <https://developer.android.com/ndk/guides/ndk-stack>.')
    parser.add_argument(
        '-sym',
        '--sym',
        dest='symbol_dir',
        required=True,  # TODO: default to '.'?
        help='directory containing unstripped .so files')
    parser.add_argument(
        '-i',
        '-dump',
        '--dump',
        dest='input',
        default='-',
        type=argparse.FileType('r'),
        help='input filename')
    args = parser.parse_args()

    if not os.path.exists(args.symbol_dir):
        sys.exit('{} does not exist!\n'.format(args.symbol_dir))

    cmd = [
        find_llvm_symbolizer(), '--demangle', '--functions=linkage',
        '--inlining=true', '--use-symbol-table=true'
    ]
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)

    banner = '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'
    in_crash = False
    saw_frame = False
    for line in args.input:
        line = line.rstrip()

        if not in_crash:
            if banner in line:
                in_crash = True
                saw_frame = False
                print('********** Crash dump: **********')
            continue

        for tag in ['Build fingerprint:', 'Abort message:']:
            if tag in line:
                print(line[line.find(tag):])
                continue

        # See Backtrace::FormatFrameData in libbacktrace.
        # We're deliberately very loose because NDK users are likely to be
        # looking at crashes on ancient OS releases.
        # TODO: support asan stacks too?
        m = re.match('.* +(#[0-9]+) +pc ([0-9a-f]+) +(([^ ]+).*)', line)
        if m:
            saw_frame = True
            frame = m.group(1)
            pc = m.group(2)
            tail = m.group(3)
            lib = m.group(4)

            lib_path = os.path.join(args.symbol_dir, os.path.basename(lib))

            out_line = '%s 0x%s %s' % (frame, pc, tail)
            indent = (out_line.find('(') + 1) * ' '
            print(out_line)
            if os.path.exists(lib_path):
                print('"%s" 0x%s' % (lib_path, pc), file=proc.stdin)
                while True:
                    symbolizer_output = proc.stdout.readline().rstrip()
                    if not symbolizer_output:
                        break
                    # TODO: rewrite file names based on a source path?
                    print('%s%s' % (indent, symbolizer_output))
        elif saw_frame:
            in_crash = False
            print('Crash dump is completed\n')

    # for asan
    asan_begin = re.compile(r".*==\d+==ERROR:\sAddressSanitizer:.*")
    asan_end = re.compile(r".*==\d+==ABORTING.*")
    asan_trace = re.compile(r".*\s+#\d+\s(0x[0-9a-fA-F]+)\s+\((.*)\+(0x[0-9a-fA-F]+)\).*")
    asan_trace_header = re.compile(r".*WRITE of size.*"
                                   r"|.*READ of size.*"
                                   r"|.*Thread .* created by .* here:.*"
                                   r"|.*freed by thread .* here:.*"
                                   r"|.*previously allocated by thread .* here:.*")

    proc.stdout.flush()
    args.input.seek(0)

    in_asan = False
    for line in args.input:
        line = line.rstrip()

        if not in_asan:
            m = asan_begin.match(line)
            if m:
                in_asan = True
                print("##############################ASAN BEGIN##############################")
                continue

        m = asan_trace_header.match(line)
        if m:
            print(line)
            continue

        m = asan_trace.match(line)
        if m:
            pc = m.group(1)
            lib = m.group(2)
            addr = m.group(3)

            print(line)
            lib_path = os.path.join(args.symbol_dir, os.path.basename(lib))
            if os.path.exists(lib_path):
                print('"%s" %s' % (lib_path, addr), file=proc.stdin)
                print('--------------------------------------------------------------------------------------------------------------------------------------------------------------------')
                while True:
                    symbolizer_output = proc.stdout.readline().rstrip()
                    if not symbolizer_output:
                        break
                    print('-->       %s        <--' % symbolizer_output)
                print('')
        elif asan_end.match(line):
            print("##############################ASAN END##############################")
            in_asan = False
            continue


if __name__ == "__main__":
    main()
