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