| #!/usr/bin/env python3 |
| # |
| # Copyright 2020 - 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. |
| |
| import logging |
| from pathlib import Path |
| import psutil |
| import re |
| import subprocess |
| from typing import Container |
| from collections import deque |
| |
| |
| class TerminalColor: |
| RED = "\033[31;1m" |
| BLUE = "\033[34;1m" |
| YELLOW = "\033[33;1m" |
| MAGENTA = "\033[35;1m" |
| END = "\033[0m" |
| |
| |
| def is_subprocess_alive(process, timeout_seconds=1): |
| """ |
| Check if a process is alive for at least timeout_seconds |
| :param process: a Popen object that represent a subprocess |
| :param timeout_seconds: process needs to be alive for at least |
| timeout_seconds |
| :return: True if process is alive for at least timeout_seconds |
| """ |
| try: |
| process.wait(timeout=timeout_seconds) |
| return False |
| except subprocess.TimeoutExpired as exp: |
| return True |
| |
| |
| def get_gd_root(): |
| """ |
| Return the root of the GD test library |
| |
| GD root is the parent directory of cert |
| :return: root directory string of gd test library |
| """ |
| return str(Path(__file__).absolute().parents[1]) |
| |
| |
| def make_ports_available(ports: Container[int], timeout_seconds=10): |
| """Make sure a list of ports are available |
| kill occupying process if possible |
| :param ports: list of target ports |
| :param timeout_seconds: number of seconds to wait when killing processes |
| :return: True on success, False on failure |
| """ |
| if not ports: |
| logging.warning("Empty ports is given to make_ports_available()") |
| return True |
| # Get connections whose state are in LISTEN only |
| # Connections in other states won't affect binding as SO_REUSEADDR is used |
| listening_conns_for_port = filter( |
| lambda conn: (conn and conn.status == psutil.CONN_LISTEN and conn.laddr and conn.laddr.port in ports), |
| psutil.net_connections()) |
| success = True |
| killed_pids = set() |
| for conn in listening_conns_for_port: |
| logging.warning("Freeing port %d used by %s" % (conn.laddr.port, str(conn))) |
| if not conn.pid: |
| logging.error("Failed to kill process occupying port %d due to lack of pid" % conn.laddr.port) |
| success = False |
| continue |
| logging.warning("Killing pid %d that is using port port %d" % (conn.pid, conn.laddr.port)) |
| if conn.pid in killed_pids: |
| logging.warning("Pid %d is already killed in previous iteration" % (conn.pid)) |
| continue |
| try: |
| process = psutil.Process(conn.pid) |
| process.kill() |
| process.wait(timeout=timeout_seconds) |
| killed_pids.add(conn.pid) |
| except psutil.NoSuchProcess: |
| logging.warning("Pid %d is already dead before trying to kill it" % (conn.pid)) |
| killed_pids.add(conn.pid) |
| continue |
| except psutil.TimeoutExpired: |
| logging.error("SIGKILL timeout after %d seconds for pid %d" % (timeout_seconds, conn.pid)) |
| success = False |
| break |
| return success |
| |
| |
| # e.g. 2020-05-06 16:02:04.216 bt - system/bt/gd/facade/facade_main.cc:79 - crash_callback: #03 pc 0000000000013520 /lib/x86_64-linux-gnu/libpthread-2.29.so |
| HOST_CRASH_LINE_REGEX = re.compile(r"^.* - crash_callback: (?P<line>.*)$") |
| HOST_ABORT_HEADER = "Process crashed, signal: Aborted" |
| ASAN_OUTPUT_START_REGEX = re.compile(r"^==.*AddressSanitizer.*$") |
| |
| |
| def read_crash_snippet_and_log_tail(logpath): |
| """ |
| Get crash snippet if regex matched or last 20 lines of log |
| :return: crash_snippet, log_tail_20 |
| 1) crash snippet without timestamp in one string; |
| 2) last 20 lines of log in one string; |
| """ |
| gd_root_prefix = get_gd_root() + "/" |
| abort_line = None |
| last_20_lines = deque(maxlen=20) |
| crash_log_lines = [] |
| asan = False |
| asan_lines = [] |
| |
| try: |
| with open(logpath) as f: |
| for _, line in enumerate(f): |
| last_20_lines.append(line) |
| asan_match = ASAN_OUTPUT_START_REGEX.match(line) |
| if asan or asan_match: |
| asan_lines.append(line) |
| asan = True |
| continue |
| |
| host_crash_match = HOST_CRASH_LINE_REGEX.match(line) |
| if host_crash_match: |
| crash_line = host_crash_match.group("line").replace(gd_root_prefix, "") |
| if HOST_ABORT_HEADER in crash_line \ |
| and len(last_20_lines) > 1: |
| abort_line = last_20_lines[-2] |
| crash_log_lines.append(crash_line) |
| except EnvironmentError: |
| logging.error("Cannot open backing log file at {}".format(logpath)) |
| return None, None |
| |
| log_tail_20 = "".join(last_20_lines) |
| crash_snippet = "" |
| if abort_line is not None: |
| crash_snippet += "abort log line:\n\n%s\n" % abort_line |
| crash_snippet += "\n".join(crash_log_lines) |
| |
| if len(asan_lines) > 0: |
| return "".join(asan_lines), log_tail_20 |
| |
| if len(crash_log_lines) > 0: |
| return crash_snippet, log_tail_20 |
| |
| return None, log_tail_20 |