1#!/usr/bin/env python3.4
2#
3#   Copyright 2018 - 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
18import re
19import threading
20import time
21
22from acts import utils
23
24
25class ErrorLogger(logging.LoggerAdapter):
26    """A logger for a given error report."""
27
28    def __init__(self, label):
29        self.label = label
30        super(ErrorLogger, self).__init__(logging.getLogger(), {})
31
32    def process(self, msg, kwargs):
33        """Transforms a log message to be in a given format."""
34        return '[Error Report|%s] %s' % (self.label, msg), kwargs
35
36
37class ErrorReporter(object):
38    """A class that reports errors and diagnoses possible points of failure.
39
40    Attributes:
41        max_reports: The maximum number of reports that should be reported.
42            Defaulted to 1 to prevent multiple reports from reporting at the
43            same time over one another.
44        name: The name of the report to be used in the error logs.
45    """
46
47    def __init__(self, name, max_reports=1):
48        """Creates an error report.
49
50        Args:
51            name: The name of the error report.
52            max_reports: Sets the maximum number of reports to this value.
53        """
54        self.name = name
55        self.max_reports = max_reports
56        self._ticket_number = 0
57        self._ticket_lock = threading.Lock()
58        self._current_request_count = 0
59        self._accept_requests = True
60
61    def create_error_report(self, sl4a_manager, sl4a_session, rpc_connection):
62        """Creates an error report, if possible.
63
64        Returns:
65            False iff a report cannot be created.
66        """
67        if not self._accept_requests:
68            return False
69
70        self._current_request_count += 1
71
72        try:
73            ticket = self._get_report_ticket()
74            if not ticket:
75                return False
76
77            report = ErrorLogger('%s|%s' % (self.name, ticket))
78
79            (self.report_on_adb(sl4a_manager.adb, report)
80             and self.report_device_processes(sl4a_manager.adb, report) and
81             self.report_sl4a_state(rpc_connection, sl4a_manager.adb, report)
82             and self.report_sl4a_session(sl4a_manager, sl4a_session, report))
83
84            return True
85        finally:
86            self._current_request_count -= 1
87
88    def report_on_adb(self, adb, report):
89        """Creates an error report for ADB. Returns false if ADB has failed."""
90        adb_uptime = utils.get_process_uptime('adb')
91        if adb_uptime:
92            report.info('The adb daemon has an uptime of %s '
93                        '([[dd-]hh:]mm:ss).' % adb_uptime)
94        else:
95            report.warning('The adb daemon (on the host machine) is not '
96                           'running. All forwarded ports have been removed.')
97            return False
98
99        devices_output = adb.devices()
100        if adb.serial not in devices_output:
101            report.warning(
102                'This device cannot be found by ADB. The device may have shut '
103                'down or disconnected.')
104            return False
105        elif re.findall(r'%s\s+offline' % adb.serial, devices_output):
106            report.warning(
107                'The device is marked as offline in ADB. We are no longer able '
108                'to access the device.')
109            return False
110        else:
111            report.info(
112                'The device is online and accessible through ADB calls.')
113        return True
114
115    def report_device_processes(self, adb, report):
116        """Creates an error report for the device's required processes.
117
118        Returns:
119            False iff user-apks cannot be communicated with over tcp.
120        """
121        zygote_uptime = utils.get_device_process_uptime(adb, 'zygote')
122        if zygote_uptime:
123            report.info(
124                'Zygote has been running for %s ([[dd-]hh:]mm:ss). If this '
125                'value is low, the phone may have recently crashed.' %
126                zygote_uptime)
127        else:
128            report.warning(
129                'Zygote has been killed. It is likely the Android Runtime has '
130                'crashed. Check the bugreport/logcat for more information.')
131            return False
132
133        netd_uptime = utils.get_device_process_uptime(adb, 'netd')
134        if netd_uptime:
135            report.info(
136                'Netd has been running for %s ([[dd-]hh:]mm:ss). If this '
137                'value is low, the phone may have recently crashed.' %
138                zygote_uptime)
139        else:
140            report.warning(
141                'Netd has been killed. The Android Runtime may have crashed. '
142                'Check the bugreport/logcat for more information.')
143            return False
144
145        adbd_uptime = utils.get_device_process_uptime(adb, 'adbd')
146        if netd_uptime:
147            report.info(
148                'Adbd has been running for %s ([[dd-]hh:]mm:ss). If this '
149                'value is low, the phone may have recently crashed.' %
150                adbd_uptime)
151        else:
152            report.warning('Adbd is not running.')
153            return False
154        return True
155
156    def report_sl4a_state(self, rpc_connection, adb, report):
157        """Creates an error report for the state of SL4A."""
158        report.info(
159            'Diagnosing Failure over connection %s.' % rpc_connection.ports)
160
161        ports = rpc_connection.ports
162        forwarded_ports_output = adb.forward('--list')
163
164        expected_output = '%s tcp:%s tcp:%s' % (adb.serial,
165                                                ports.forwarded_port,
166                                                ports.server_port)
167        if expected_output not in forwarded_ports_output:
168            formatted_output = re.sub(
169                '^', '    ', forwarded_ports_output, flags=re.MULTILINE)
170            report.warning(
171                'The forwarded port for the failed RpcConnection is missing.\n'
172                'Expected:\n    %s\nBut found:\n%s' % (expected_output,
173                                                       formatted_output))
174            return False
175        else:
176            report.info('The connection port has been properly forwarded to '
177                        'the device.')
178
179        sl4a_uptime = utils.get_device_process_uptime(
180            adb, 'com.googlecode.android_scripting')
181        if sl4a_uptime:
182            report.info(
183                'SL4A has been running for %s ([[dd-]hh:]mm:ss). If this '
184                'value is lower than the test case, it must have been '
185                'restarted during the test.' % sl4a_uptime)
186        else:
187            report.warning(
188                'The SL4A scripting service is not running. SL4A may have '
189                'crashed, or have been terminated by the Android Runtime.')
190            return False
191        return True
192
193    def report_sl4a_session(self, sl4a_manager, session, report):
194        """Reports the state of an SL4A session."""
195        if session.server_port not in sl4a_manager.sl4a_ports_in_use:
196            report.warning('SL4A server port %s not found in set of open '
197                           'ports %s' % (session.server_port,
198                                         sl4a_manager.sl4a_ports_in_use))
199            return False
200
201        if session not in sl4a_manager.sessions.values():
202            report.warning('SL4A session %s over port %s is not managed by '
203                           'the SL4A Manager. This session is already dead.' %
204                           (session.uid, session.server_port))
205            return False
206        return True
207
208    def finalize_reports(self):
209        self._accept_requests = False
210        while self._current_request_count > 0:
211            # Wait for other threads to finish.
212            time.sleep(.1)
213
214    def _get_report_ticket(self):
215        """Returns the next ticket, or none if all tickets have been used."""
216        with self._ticket_lock:
217            self._ticket_number += 1
218            ticket_number = self._ticket_number
219
220        if ticket_number <= self.max_reports:
221            return ticket_number
222        else:
223            return None
224