blob: 0829d01d2131a7385136fd639a05d8d6da60331e [file] [log] [blame]
#!/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