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(
113            lambda msg: '[SL4A Manager|%s] %s' % (adb.serial, msg))
114        self.sessions = {}
115        self._started = False
116        self.error_reporter = error_reporter.ErrorReporter(
117            'SL4A %s' % 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' % (device_port,
165                                                   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(
207                'pm path com.googlecode\.android_scripting',
208                ignore_status=True))
209
210    def start_sl4a_service(self):
211        """Starts the SL4A Service on the device.
212
213        For starting an RPC server, use start_sl4a_server() instead.
214        """
215        # Verify SL4A is installed.
216        if not self._started:
217            self._started = True
218            if not self.is_sl4a_installed():
219                raise rpc_client.Sl4aNotInstalledError(
220                    'SL4A is not installed on device %s' % self.adb.serial)
221            if self.adb.shell(
222                    '(ps | grep "S com.googlecode.android_scripting") || true'):
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                       server_port=None):
248        """Creates an SL4A server with the given ports if possible.
249
250        The ports are not guaranteed to be available for use. If the port
251        asked for is not available, this will be logged, and the port will
252        be randomized.
253
254        Args:
255            client_port: The port on the host machine
256            server_port: The port on the Android device.
257            max_connections: The max number of client connections for the
258                session.
259
260        Returns:
261            A new Sl4aServer instance.
262        """
263        if server_port is None:
264            # If a session already exists, use the same server.
265            if len(self.sessions) > 0:
266                server_port = self.sessions[sorted(
267                    self.sessions.keys())[0]].server_port
268            # Otherwise, open a new server on a random port.
269            else:
270                server_port = 0
271        self.start_sl4a_service()
272        session = sl4a_session.Sl4aSession(
273            self.adb,
274            client_port,
275            server_port,
276            self.obtain_sl4a_server,
277            self.diagnose_failure,
278            max_connections=max_connections)
279        self.sessions[session.uid] = session
280        return session
281
282    def stop_service(self):
283        """Stops The SL4A Service."""
284        self._started = False
285
286    def terminate_all_sessions(self):
287        """Terminates all SL4A sessions gracefully."""
288        self.error_reporter.finalize_reports()
289        for _, session in self.sessions.items():
290            session.terminate()
291        self.sessions = {}
292        self._close_all_ports()
293
294    def _close_all_ports(self, try_interval=ATTEMPT_INTERVAL):
295        """Closes all ports opened on SL4A."""
296        ports = self._get_all_ports()
297        for port in set.union(self._sl4a_ports, ports):
298            self.adb.shell(_SL4A_CLOSE_SERVER_CMD % port)
299        time_left = MAX_WAIT_ON_SERVER_SECONDS
300        while time_left > 0 and self._get_open_listening_port():
301            time.sleep(try_interval)
302            time_left -= try_interval
303
304        if time_left <= 0:
305            self.log.warning(
306                'Unable to close all un-managed servers! Server ports that are '
307                'still open are %s' % self._get_open_listening_port())
308        self._sl4a_ports = set()
309