# Copyright 2017 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import functools
import logging
import os
import subprocess
import threading

_CHROME_SRC = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
_LLVM_SYMBOLIZER_PATH = os.path.join(
    _CHROME_SRC,
    'third_party',
    'llvm-build',
    'Release+Asserts',
    'bin',
    'llvm-symbolizer',
)

_UNKNOWN = '<UNKNOWN>'

_ELF_MAGIC_HEADER_BYTES = b'\x7f\x45\x4c\x46'


@functools.lru_cache
def IsValidLLVMSymbolizerTarget(file_path):
    """Verify the passed file is a valid target for llvm-symbolization

    Args:
      file_path: Path to a file to be checked

    Return:
      True if the file exists and has the correct ELF header, False otherwise
    """
    try:
        with open(file_path, 'rb') as f:
            header_bytes = f.read(4)
            return header_bytes == _ELF_MAGIC_HEADER_BYTES
    except IOError:
        return False


class LLVMSymbolizer(object):
    def __init__(self):
        """Create a LLVMSymbolizer instance that interacts with the llvm symbolizer.

        The purpose of the LLVMSymbolizer is to get function names and line
        numbers of an address from the symbols library.
        """
        self._llvm_symbolizer_subprocess = None
        self._llvm_symbolizer_parameters = [
            '--functions',
            '--demangle',
            '--inlines',
        ]

        # Allow only one thread to call GetSymbolInformation at a time.
        self._lock = threading.Lock()

    def Start(self):
        """Start the llvm symbolizer subprocess.

        Create a subprocess of the llvm symbolizer executable, which will be used
        to retrieve function names etc.
        """
        if os.path.isfile(_LLVM_SYMBOLIZER_PATH):
            self._llvm_symbolizer_subprocess = subprocess.Popen(
                [_LLVM_SYMBOLIZER_PATH] + self._llvm_symbolizer_parameters,
                stdout=subprocess.PIPE,
                stdin=subprocess.PIPE,
                universal_newlines=True,
            )
        else:
            logging.error(
                'Cannot find llvm_symbolizer here: %s.' % _LLVM_SYMBOLIZER_PATH
            )
            self._llvm_symbolizer_subprocess = None

    def Close(self):
        """Close the llvm symbolizer subprocess.

        Close the subprocess by closing stdin, stdout and killing the subprocess.
        """
        with self._lock:
            if self._llvm_symbolizer_subprocess:
                self._llvm_symbolizer_subprocess.kill()
                self._llvm_symbolizer_subprocess = None

    def __enter__(self):
        """Start the llvm symbolizer subprocess."""
        self.Start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Close the llvm symbolizer subprocess."""
        self.Close()

    def GetSymbolInformation(self, lib, addr):
        """Return the corresponding function names and line numbers.

        Args:
          lib: library to search for info.
          addr: address to look for info.

        Returns:
          A triplet of address, module-name and list of symbols
        """
        if self._llvm_symbolizer_subprocess is None:
            logging.error(
                'Can\'t run llvm-symbolizer! '
                + 'Subprocess for llvm-symbolizer has not been started!'
            )
            return [(_UNKNOWN, lib)]

        if not lib:
            logging.error('Can\'t run llvm-symbolizer! No target is given!')
            return [(_UNKNOWN, lib)]

        if not IsValidLLVMSymbolizerTarget(lib):
            logging.error(
                'Can\'t run llvm-symbolizer! '
                + 'Given binary is not a valid target. path=%s',
                lib,
            )
            return [(_UNKNOWN, lib)]

        proc = self._llvm_symbolizer_subprocess
        with self._lock:
            proc.stdin.write('%s %s\n' % (lib, hex(addr)))
            proc.stdin.flush()
            result = []
            # Read until an empty line is observed, which indicates the end of the
            # output. Each line with a function name is always followed by one line
            # with the corresponding line number.
            while True:
                line = proc.stdout.readline()
                if line != '\n':
                    line_numbers = proc.stdout.readline()
                    result.append((line[:-1], line_numbers[:-1]))
                else:
                    return result

    @staticmethod
    def IsValidTarget(path):
        return IsValidLLVMSymbolizerTarget(path)
