| #!/usr/bin/env python3 |
| # |
| # Copyright 2022 The Fuchsia Authors |
| # |
| # 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 |
| import re |
| import threading |
| import time |
| |
| from antlion import utils |
| |
| |
| class ErrorLogger(logging.LoggerAdapter): |
| """A logger for a given error report.""" |
| |
| def __init__(self, label): |
| self.label = label |
| super(ErrorLogger, self).__init__(logging.getLogger(), {}) |
| |
| def process(self, msg, kwargs): |
| """Transforms a log message to be in a given format.""" |
| return f"[Error Report|{self.label}] {msg}", kwargs |
| |
| |
| class ErrorReporter(object): |
| """A class that reports errors and diagnoses possible points of failure. |
| |
| Attributes: |
| max_reports: The maximum number of reports that should be reported. |
| Defaulted to 1 to prevent multiple reports from reporting at the |
| same time over one another. |
| name: The name of the report to be used in the error logs. |
| """ |
| |
| def __init__(self, name, max_reports=1): |
| """Creates an error report. |
| |
| Args: |
| name: The name of the error report. |
| max_reports: Sets the maximum number of reports to this value. |
| """ |
| self.name = name |
| self.max_reports = max_reports |
| self._ticket_number = 0 |
| self._ticket_lock = threading.Lock() |
| self._current_request_count = 0 |
| self._accept_requests = True |
| |
| def create_error_report(self, sl4a_manager, sl4a_session, rpc_connection): |
| """Creates an error report, if possible. |
| |
| Returns: |
| False iff a report cannot be created. |
| """ |
| if not self._accept_requests: |
| return False |
| |
| self._current_request_count += 1 |
| |
| try: |
| ticket = self._get_report_ticket() |
| if not ticket: |
| return False |
| |
| report = ErrorLogger(f"{self.name}|{ticket}") |
| report.info("Creating error report.") |
| |
| ( |
| self.report_on_adb(sl4a_manager.adb, report) |
| and self.report_device_processes(sl4a_manager.adb, report) |
| and self.report_sl4a_state(rpc_connection, sl4a_manager.adb, report) |
| and self.report_sl4a_session(sl4a_manager, sl4a_session, report) |
| ) |
| |
| return True |
| finally: |
| self._current_request_count -= 1 |
| |
| def report_on_adb(self, adb, report): |
| """Creates an error report for ADB. Returns false if ADB has failed.""" |
| adb_uptime = utils.get_command_uptime('"adb .* server"') |
| if adb_uptime: |
| report.info( |
| f"The adb daemon has an uptime of {adb_uptime} ([[dd-]hh:]mm:ss)." |
| ) |
| else: |
| report.warning( |
| "The adb daemon (on the host machine) is not " |
| "running. All forwarded ports have been removed." |
| ) |
| return False |
| |
| devices_output = adb.devices() |
| if adb.serial not in devices_output: |
| report.warning( |
| "This device cannot be found by ADB. The device may have shut " |
| "down or disconnected." |
| ) |
| return False |
| elif re.findall(r"%s\s+offline" % adb.serial, devices_output): |
| report.warning( |
| "The device is marked as offline in ADB. We are no longer able " |
| "to access the device." |
| ) |
| return False |
| else: |
| report.info("The device is online and accessible through ADB calls.") |
| return True |
| |
| def report_device_processes(self, adb, report): |
| """Creates an error report for the device's required processes. |
| |
| Returns: |
| False iff user-apks cannot be communicated with over tcp. |
| """ |
| zygote_uptime = utils.get_device_process_uptime(adb, "zygote") |
| if zygote_uptime: |
| report.info( |
| "Zygote has been running for %s ([[dd-]hh:]mm:ss). If this " |
| "value is low, the phone may have recently crashed." % zygote_uptime |
| ) |
| else: |
| report.warning( |
| "Zygote has been killed. It is likely the Android Runtime has " |
| "crashed. Check the bugreport/logcat for more information." |
| ) |
| return False |
| |
| netd_uptime = utils.get_device_process_uptime(adb, "netd") |
| if netd_uptime: |
| report.info( |
| "Netd has been running for %s ([[dd-]hh:]mm:ss). If this " |
| "value is low, the phone may have recently crashed." % zygote_uptime |
| ) |
| else: |
| report.warning( |
| "Netd has been killed. The Android Runtime may have crashed. " |
| "Check the bugreport/logcat for more information." |
| ) |
| return False |
| |
| adbd_uptime = utils.get_device_process_uptime(adb, "adbd") |
| if netd_uptime: |
| report.info( |
| "Adbd has been running for %s ([[dd-]hh:]mm:ss). If this " |
| "value is low, the phone may have recently crashed." % adbd_uptime |
| ) |
| else: |
| report.warning("Adbd is not running.") |
| return False |
| return True |
| |
| def report_sl4a_state(self, rpc_connection, adb, report): |
| """Creates an error report for the state of SL4A.""" |
| report.info(f"Diagnosing Failure over connection {rpc_connection.ports}.") |
| |
| ports = rpc_connection.ports |
| forwarded_ports_output = adb.forward("--list") |
| |
| expected_output = "%s tcp:%s tcp:%s" % ( |
| adb.serial, |
| ports.forwarded_port, |
| ports.server_port, |
| ) |
| if expected_output not in forwarded_ports_output: |
| formatted_output = re.sub( |
| "^", " ", forwarded_ports_output, flags=re.MULTILINE |
| ) |
| report.warning( |
| "The forwarded port for the failed RpcConnection is missing.\n" |
| "Expected:\n %s\nBut found:\n%s" |
| % (expected_output, formatted_output) |
| ) |
| return False |
| else: |
| report.info( |
| "The connection port has been properly forwarded to " "the device." |
| ) |
| |
| sl4a_uptime = utils.get_device_process_uptime( |
| adb, "com.googlecode.android_scripting" |
| ) |
| if sl4a_uptime: |
| report.info( |
| "SL4A has been running for %s ([[dd-]hh:]mm:ss). If this " |
| "value is lower than the test case, it must have been " |
| "restarted during the test." % sl4a_uptime |
| ) |
| else: |
| report.warning( |
| "The SL4A scripting service is not running. SL4A may have " |
| "crashed, or have been terminated by the Android Runtime." |
| ) |
| return False |
| return True |
| |
| def report_sl4a_session(self, sl4a_manager, session, report): |
| """Reports the state of an SL4A session.""" |
| if session.server_port not in sl4a_manager.sl4a_ports_in_use: |
| report.warning( |
| "SL4A server port %s not found in set of open " |
| "ports %s" % (session.server_port, sl4a_manager.sl4a_ports_in_use) |
| ) |
| return False |
| |
| if session not in sl4a_manager.sessions.values(): |
| report.warning( |
| "SL4A session %s over port %s is not managed by " |
| "the SL4A Manager. This session is already dead." |
| % (session.uid, session.server_port) |
| ) |
| return False |
| return True |
| |
| def finalize_reports(self): |
| self._accept_requests = False |
| while self._current_request_count > 0: |
| # Wait for other threads to finish. |
| time.sleep(0.1) |
| |
| def _get_report_ticket(self): |
| """Returns the next ticket, or none if all tickets have been used.""" |
| logging.debug("Getting ticket for SL4A error report.") |
| with self._ticket_lock: |
| self._ticket_number += 1 |
| ticket_number = self._ticket_number |
| |
| if ticket_number <= self.max_reports: |
| return ticket_number |
| else: |
| return None |