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