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