Home | History | Annotate | Download | only in sl4a_lib
      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 import threading
     17 
     18 import time
     19 
     20 from acts import logger
     21 from acts.controllers.sl4a_lib import rpc_client
     22 from acts.controllers.sl4a_lib import sl4a_session
     23 from acts.controllers.sl4a_lib import error_reporter
     24 
     25 ATTEMPT_INTERVAL = .25
     26 MAX_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 
     75 def 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 
     92 class 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.MissingSl4AError(
    220                     'SL4A is not installed on device %s' % self.adb.serial)
    221             if self.adb.shell(
    222                     'ps | grep "S com.googlecode.android_scripting"'):
    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 terminate_all_sessions(self):
    283         """Terminates all SL4A sessions gracefully."""
    284         self.error_reporter.finalize_reports()
    285         for _, session in self.sessions.items():
    286             session.terminate()
    287         self.sessions = {}
    288         self._close_all_ports()
    289 
    290     def _close_all_ports(self, try_interval=ATTEMPT_INTERVAL):
    291         """Closes all ports opened on SL4A."""
    292         ports = self._get_all_ports()
    293         for port in set.union(self._sl4a_ports, ports):
    294             self.adb.shell(_SL4A_CLOSE_SERVER_CMD % port)
    295         time_left = MAX_WAIT_ON_SERVER_SECONDS
    296         while time_left > 0 and self._get_open_listening_port():
    297             time.sleep(try_interval)
    298             time_left -= try_interval
    299 
    300         if time_left <= 0:
    301             self.log.warning(
    302                 'Unable to close all un-managed servers! Server ports that are '
    303                 'still open are %s' % self._get_open_listening_port())
    304         self._sl4a_ports = set()
    305