1#!/usr/bin/env python3
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.
16import threading
17
18import time
19
20from acts import logger
21from acts.controllers.sl4a_lib import rpc_client
22from acts.controllers.sl4a_lib import sl4a_session
23from acts.controllers.sl4a_lib import error_reporter
24
25ATTEMPT_INTERVAL = .25
26MAX_WAIT_ON_SERVER_SECONDS = 5
27
28_SL4A_LAUNCH_SERVER_CMD = (
29    'am startservice -a com.googlecode.android_scripting.action.LAUNCH_SERVER '
30    '--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s '
31    'com.googlecode.android_scripting/.service.ScriptingLayerService')
32
33_SL4A_CLOSE_SERVER_CMD = (
34    'am startservice -a com.googlecode.android_scripting.action.KILL_PROCESS '
35    '--ei com.googlecode.android_scripting.extra.PROXY_PORT %s '
36    'com.googlecode.android_scripting/.service.ScriptingLayerService')
37
38# The command for finding SL4A's server port as root.
39_SL4A_ROOT_FIND_PORT_CMD = (
40    # Get all open, listening ports, and their process names
41    'ss -l -p -n | '
42    # Find all open TCP ports for SL4A
43    'grep "tcp.*droid_scripting" | '
44    # Shorten all whitespace to a single space character
45    'tr -s " " | '
46    # Grab the 5th column (which is server:port)
47    'cut -d " " -f 5 |'
48    # Only grab the port
49    'sed s/.*://g')
50
51# The command for finding SL4A's server port without root.
52_SL4A_USER_FIND_PORT_CMD = (
53    # Get all open, listening ports, and their process names
54    'ss -l -p -n | '
55    # Find all open ports exposed to the public. This can produce false
56    # positives since users cannot read the process associated with the port.
57    'grep -e "tcp.*::ffff:127\.0\.0\.1:" | '
58    # Shorten all whitespace to a single space character
59    'tr -s " " | '
60    # Grab the 5th column (which is server:port)
61    'cut -d " " -f 5 |'
62    # Only grab the port
63    'sed s/.*://g')
64
65# The command that begins the SL4A ScriptingLayerService.
66_SL4A_START_SERVICE_CMD = (
67    'am startservice '
68    'com.googlecode.android_scripting/.service.ScriptingLayerService')
69
70# Maps device serials to their SL4A Manager. This is done to prevent multiple
71# Sl4aManagers from existing for the same device.
72_all_sl4a_managers = {}
73
74
75def create_sl4a_manager(adb):
76    """Creates and returns an SL4AManager for the given device.
77
78    Args:
79        adb: A reference to the device's AdbProxy.
80    """
81    if adb.serial in _all_sl4a_managers:
82        _all_sl4a_managers[adb.serial].log.warning(
83            'Attempted to return multiple SL4AManagers on the same device. '
84            'Returning pre-existing SL4AManager instead.')
85        return _all_sl4a_managers[adb.serial]
86    else:
87        manager = Sl4aManager(adb)
88        _all_sl4a_managers[adb.serial] = manager
89        return manager
90
91
92class Sl4aManager(object):
93    """A manager for SL4A Clients to a given AndroidDevice.
94
95    SL4A is a single APK that can host multiple RPC servers at a time. This
96    class manages each server connection over ADB, and will gracefully
97    terminate the apk during cleanup.
98
99    Attributes:
100        _listen_for_port_lock: A lock for preventing multiple threads from
101            potentially mixing up requested ports.
102        _sl4a_ports: A set of all known SL4A server ports in use.
103        adb: A reference to the AndroidDevice's AdbProxy.
104        log: The logger for this object.
105        sessions: A dictionary of session_ids to sessions.
106    """
107
108    def __init__(self, adb):
109        self._listen_for_port_lock = threading.Lock()
110        self._sl4a_ports = set()
111        self.adb = adb
112        self.log = logger.create_logger(lambda msg: '[SL4A Manager|%s] %s' % (
113            adb.serial, msg))
114        self.sessions = {}
115        self._started = False
116        self.error_reporter = error_reporter.ErrorReporter('SL4A %s' %
117                                                           adb.serial)
118
119    @property
120    def sl4a_ports_in_use(self):
121        """Returns a list of all server ports used by SL4A servers."""
122        return set([session.server_port for session in self.sessions.values()])
123
124    def diagnose_failure(self, session, connection):
125        """Diagnoses all potential known reasons SL4A can fail.
126
127        Assumes the failure happened on an RPC call, which verifies the state
128        of ADB/device."""
129        self.error_reporter.create_error_report(self, session, connection)
130
131    def start_sl4a_server(self, device_port, try_interval=ATTEMPT_INTERVAL):
132        """Opens a server socket connection on SL4A.
133
134        Args:
135            device_port: The expected port for SL4A to open on. Note that in
136                many cases, this will be different than the port returned by
137                this method.
138            try_interval: The amount of seconds between attempts at finding an
139                opened port on the AndroidDevice.
140
141        Returns:
142            The port number on the device the SL4A server is open on.
143
144        Raises:
145            Sl4aConnectionError if SL4A's opened port cannot be found.
146        """
147        # Launch a server through SL4A.
148        self.adb.shell(_SL4A_LAUNCH_SERVER_CMD % device_port)
149
150        # There is a chance that the server has not come up yet by the time the
151        # launch command has finished. Try to read get the listening port again
152        # after a small amount of time.
153        time_left = MAX_WAIT_ON_SERVER_SECONDS
154        while time_left > 0:
155            port = self._get_open_listening_port()
156            if port is None:
157                time.sleep(try_interval)
158                time_left -= try_interval
159            else:
160                return port
161
162        raise rpc_client.Sl4aConnectionError(
163            'Unable to find a valid open port for a new server connection. '
164            'Expected port: %s. Open ports: %s' %
165            (device_port, self._sl4a_ports))
166
167    def _get_all_ports_command(self):
168        """Returns the list of all ports from the command to get ports."""
169        is_root = True
170        if not self.adb.is_root():
171            is_root = self.adb.ensure_root()
172
173        if is_root:
174            return _SL4A_ROOT_FIND_PORT_CMD
175        else:
176            # TODO(markdr): When root is unavailable, search logcat output for
177            #               the port the server has opened.
178            self.log.warning('Device cannot be put into root mode. SL4A '
179                             'server connections cannot be verified.')
180            return _SL4A_USER_FIND_PORT_CMD
181
182    def _get_all_ports(self):
183        return self.adb.shell(self._get_all_ports_command()).split()
184
185    def _get_open_listening_port(self):
186        """Returns any open, listening port found for SL4A.
187
188        Will return none if no port is found.
189        """
190        possible_ports = self._get_all_ports()
191        self.log.debug('SL4A Ports found: %s' % possible_ports)
192
193        # Acquire the lock. We lock this method because if multiple threads
194        # attempt to get a server at the same time, they can potentially find
195        # the same port as being open, and both attempt to connect to it.
196        with self._listen_for_port_lock:
197            for port in possible_ports:
198                if port not in self._sl4a_ports:
199                    self._sl4a_ports.add(port)
200                    return int(port)
201        return None
202
203    def is_sl4a_installed(self):
204        """Returns True if SL4A is installed on the AndroidDevice."""
205        return bool(
206            self.adb.shell('pm path com.googlecode\.android_scripting',
207                           ignore_status=True))
208
209    def start_sl4a_service(self):
210        """Starts the SL4A Service on the device.
211
212        For starting an RPC server, use start_sl4a_server() instead.
213        """
214        # Verify SL4A is installed.
215        if not self._started:
216            self._started = True
217            if not self.is_sl4a_installed():
218                raise rpc_client.Sl4aNotInstalledError(
219                    'SL4A is not installed on device %s' % self.adb.serial)
220            if self.adb.shell(
221                    '(ps | grep "S com.googlecode.android_scripting") || true'
222            ):
223                # Close all SL4A servers not opened by this manager.
224                # TODO(markdr): revert back to closing all ports after
225                # b/76147680 is resolved.
226                self.adb.shell(
227                    'kill -9 $(pidof com.googlecode.android_scripting)')
228            self.adb.shell(
229                'settings put global hidden_api_blacklist_exemptions "*"')
230            # Start the service if it is not up already.
231            self.adb.shell(_SL4A_START_SERVICE_CMD)
232
233    def obtain_sl4a_server(self, server_port):
234        """Obtain an SL4A server port.
235
236        If the port is open and valid, return it. Otherwise, open an new server
237        with the hinted server_port.
238        """
239        if server_port not in self.sl4a_ports_in_use:
240            return self.start_sl4a_server(server_port)
241        else:
242            return server_port
243
244    def create_session(self,
245                       max_connections=None,
246                       client_port=0,
247                       forwarded_port=0,
248                       server_port=None):
249        """Creates an SL4A server with the given ports if possible.
250
251        The ports are not guaranteed to be available for use. If the port
252        asked for is not available, this will be logged, and the port will
253        be randomized.
254
255        Args:
256            client_port: The client port on the host machine
257            forwarded_port: The server port on the host machine forwarded
258                            by adb from the Android device
259            server_port: The port on the Android device.
260            max_connections: The max number of client connections for the
261                session.
262
263        Returns:
264            A new Sl4aServer instance.
265        """
266        if server_port is None:
267            # If a session already exists, use the same server.
268            if len(self.sessions) > 0:
269                server_port = self.sessions[sorted(
270                    self.sessions.keys())[0]].server_port
271            # Otherwise, open a new server on a random port.
272            else:
273                server_port = 0
274        self.log.debug(
275            "Creating SL4A session client_port={}, forwarded_port={}, server_port={}"
276            .format(client_port, forwarded_port, server_port))
277        self.start_sl4a_service()
278        session = sl4a_session.Sl4aSession(self.adb,
279                                           client_port,
280                                           server_port,
281                                           self.obtain_sl4a_server,
282                                           self.diagnose_failure,
283                                           forwarded_port,
284                                           max_connections=max_connections)
285        self.sessions[session.uid] = session
286        return session
287
288    def stop_service(self):
289        """Stops The SL4A Service."""
290        self._started = False
291
292    def terminate_all_sessions(self):
293        """Terminates all SL4A sessions gracefully."""
294        self.error_reporter.finalize_reports()
295        for _, session in self.sessions.items():
296            session.terminate()
297        self.sessions = {}
298        self._close_all_ports()
299
300    def _close_all_ports(self, try_interval=ATTEMPT_INTERVAL):
301        """Closes all ports opened on SL4A."""
302        ports = self._get_all_ports()
303        for port in set.union(self._sl4a_ports, ports):
304            self.adb.shell(_SL4A_CLOSE_SERVER_CMD % port)
305        time_left = MAX_WAIT_ON_SERVER_SECONDS
306        while time_left > 0 and self._get_open_listening_port():
307            time.sleep(try_interval)
308            time_left -= try_interval
309
310        if time_left <= 0:
311            self.log.warning(
312                'Unable to close all un-managed servers! Server ports that are '
313                'still open are %s' % self._get_open_listening_port())
314        self._sl4a_ports = set()
315