1#!/usr/bin/env python3
2#
3#   Copyright 2020 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18from pathlib import Path
19import psutil
20import re
21import subprocess
22from typing import Container
23from collections import deque
24
25
26class TerminalColor:
27    RED = "\033[31;1m"
28    BLUE = "\033[34;1m"
29    YELLOW = "\033[33;1m"
30    MAGENTA = "\033[35;1m"
31    END = "\033[0m"
32
33
34def is_subprocess_alive(process, timeout_seconds=1):
35    """
36    Check if a process is alive for at least timeout_seconds
37    :param process: a Popen object that represent a subprocess
38    :param timeout_seconds: process needs to be alive for at least
39           timeout_seconds
40    :return: True if process is alive for at least timeout_seconds
41    """
42    try:
43        process.wait(timeout=timeout_seconds)
44        return False
45    except subprocess.TimeoutExpired as exp:
46        return True
47
48
49def get_gd_root():
50    """
51    Return the root of the GD test library
52
53    GD root is the parent directory of blueberry/tests/gd/cert
54    :return: root directory string of gd test library
55    """
56    return str(Path(__file__).absolute().parents[4])
57
58
59def make_ports_available(ports: Container[int], timeout_seconds=10):
60    """Make sure a list of ports are available
61    kill occupying process if possible
62    :param ports: list of target ports
63    :param timeout_seconds: number of seconds to wait when killing processes
64    :return: True on success, False on failure
65    """
66    if not ports:
67        logging.warning("Empty ports is given to make_ports_available()")
68        return True
69    # Get connections whose state are in LISTEN only
70    # Connections in other states won't affect binding as SO_REUSEADDR is used
71    listening_conns_for_port = filter(
72        lambda conn: (conn and conn.status == psutil.CONN_LISTEN and conn.laddr and conn.laddr.port in ports),
73        psutil.net_connections())
74    success = True
75    killed_pids = set()
76    for conn in listening_conns_for_port:
77        logging.warning("Freeing port %d used by %s" % (conn.laddr.port, str(conn)))
78        if not conn.pid:
79            logging.error("Failed to kill process occupying port %d due to lack of pid" % conn.laddr.port)
80            continue
81        logging.warning("Killing pid %d that is using port port %d" % (conn.pid, conn.laddr.port))
82        if conn.pid in killed_pids:
83            logging.warning("Pid %d is already killed in previous iteration" % (conn.pid))
84            continue
85        try:
86            process = psutil.Process(conn.pid)
87            process.kill()
88            process.wait(timeout=timeout_seconds)
89            killed_pids.add(conn.pid)
90        except psutil.NoSuchProcess:
91            logging.warning("Pid %d is already dead before trying to kill it" % (conn.pid))
92            killed_pids.add(conn.pid)
93            continue
94        except psutil.TimeoutExpired:
95            logging.error("SIGKILL timeout after %d seconds for pid %d" % (timeout_seconds, conn.pid))
96            success = False
97            break
98    return success
99
100
101# e.g. 2020-05-06 16:02:04.216 bt - packages/modules/Bluetooth/system/gd/facade/facade_main.cc:79 - crash_callback: #03 pc 0000000000013520  /lib/x86_64-linux-gnu/libpthread-2.29.so
102HOST_CRASH_LINE_REGEX = re.compile(r"^.* - crash_callback: (?P<line>.*)$")
103HOST_ABORT_HEADER = "Process crashed, signal: Aborted"
104ASAN_OUTPUT_START_REGEX = re.compile(r"^==.*AddressSanitizer.*$")
105
106
107def read_crash_snippet_and_log_tail(logpath):
108    """
109    Get crash snippet if regex matched or last 20 lines of log
110    :return: crash_snippet, log_tail_20
111            1) crash snippet without timestamp in one string;
112            2) last 20 lines of log in one string;
113    """
114    gd_root_prefix = get_gd_root() + "/"
115    abort_line = None
116    last_20_lines = deque(maxlen=20)
117    crash_log_lines = []
118    asan = False
119    asan_lines = []
120
121    try:
122        with open(logpath) as f:
123            for _, line in enumerate(f):
124                last_20_lines.append(line)
125                asan_match = ASAN_OUTPUT_START_REGEX.match(line)
126                if asan or asan_match:
127                    asan_lines.append(line)
128                    asan = True
129                    continue
130
131                host_crash_match = HOST_CRASH_LINE_REGEX.match(line)
132                if host_crash_match:
133                    crash_line = host_crash_match.group("line").replace(gd_root_prefix, "")
134                    if HOST_ABORT_HEADER in crash_line \
135                            and len(last_20_lines) > 1:
136                        abort_line = last_20_lines[-2]
137                    crash_log_lines.append(crash_line)
138    except EnvironmentError:
139        logging.error("Cannot open backing log file at {}".format(logpath))
140        return None, None
141
142    log_tail_20 = "".join(last_20_lines)
143    crash_snippet = ""
144    if abort_line is not None:
145        crash_snippet += "abort log line:\n\n%s\n" % abort_line
146    crash_snippet += "\n".join(crash_log_lines)
147
148    if len(asan_lines) > 0:
149        return "".join(asan_lines), log_tail_20
150
151    if len(crash_log_lines) > 0:
152        return crash_snippet, log_tail_20
153
154    return None, log_tail_20
155