1 #/usr/bin/env python3.4 2 # 3 # Copyright (C) 2009 Google Inc. 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 # use this file except in compliance with the License. You may obtain a copy of 7 # 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, WITHOUT 13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 # License for the specific language governing permissions and limitations under 15 # the License. 16 """ 17 JSON RPC interface to android scripting engine. 18 """ 19 20 from builtins import str 21 22 import json 23 import logging 24 import os 25 import socket 26 import sys 27 import threading 28 import time 29 30 from vts.utils.python.controllers import adb 31 32 HOST = os.environ.get('SL4A_HOST_ADDRESS', None) 33 PORT = os.environ.get('SL4A_HOST_PORT', 9999) 34 DEFAULT_DEVICE_SIDE_PORT = 8080 35 36 UNKNOWN_UID = -1 37 38 MAX_SL4A_WAIT_TIME = 10 39 _SL4A_LAUNCH_CMD = ( 40 "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER " 41 "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {} " 42 "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher") 43 44 45 class Error(Exception): 46 pass 47 48 49 class StartError(Error): 50 """Raised when sl4a is not able to be started.""" 51 52 53 class ApiError(Error): 54 """Raised when remote API reports an error.""" 55 56 57 class ProtocolError(Error): 58 """Raised when there is some error in exchanging data with server on device.""" 59 NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake." 60 NO_RESPONSE_FROM_SERVER = "No response from server." 61 MISMATCHED_API_ID = "Mismatched API id." 62 63 64 def start_sl4a(adb_proxy, 65 device_side_port=DEFAULT_DEVICE_SIDE_PORT, 66 wait_time=MAX_SL4A_WAIT_TIME): 67 """Starts sl4a server on the android device. 68 69 Args: 70 adb_proxy: adb.AdbProxy, The adb proxy to use to start sl4a 71 device_side_port: int, The port number to open on the device side. 72 wait_time: float, The time to wait for sl4a to come up before raising 73 an error. 74 75 Raises: 76 Error: Raised when SL4A was not able to be started. 77 """ 78 if not is_sl4a_installed(adb_proxy): 79 raise StartError("SL4A is not installed on %s" % adb_proxy.serial) 80 MAX_SL4A_WAIT_TIME = 10 81 adb_proxy.shell(_SL4A_LAUNCH_CMD.format(device_side_port)) 82 for _ in range(wait_time): 83 time.sleep(1) 84 if is_sl4a_running(adb_proxy): 85 return 86 raise StartError("SL4A failed to start on %s." % adb_proxy.serial) 87 88 89 def is_sl4a_installed(adb_proxy): 90 """Checks if sl4a is installed by querying the package path of sl4a. 91 92 Args: 93 adb: adb.AdbProxy, The adb proxy to use for checking install. 94 95 Returns: 96 True if sl4a is installed, False otherwise. 97 """ 98 try: 99 adb_proxy.shell("pm path com.googlecode.android_scripting") 100 return True 101 except adb.AdbError as e: 102 if not e.stderr: 103 return False 104 raise 105 106 107 def is_sl4a_running(adb_proxy): 108 """Checks if the sl4a app is running on an android device. 109 110 Args: 111 adb_proxy: adb.AdbProxy, The adb proxy to use for checking. 112 113 Returns: 114 True if the sl4a app is running, False otherwise. 115 """ 116 # Grep for process with a preceding S which means it is truly started. 117 out = adb_proxy.shell('ps | grep "S com.googlecode.android_scripting"') 118 if len(out) == 0: 119 return False 120 return True 121 122 123 class Sl4aCommand(object): 124 """Commands that can be invoked on the sl4a client. 125 126 INIT: Initializes a new sessions in sl4a. 127 CONTINUE: Creates a connection. 128 """ 129 INIT = 'initiate' 130 CONTINUE = 'continue' 131 132 133 class Sl4aClient(object): 134 """A sl4a client that is connected to remotely. 135 136 Connects to a remove device running sl4a. Before opening a connection 137 a port forward must be setup to go over usb. This be done using 138 adb.tcp_forward(). This is calling the shell command 139 adb forward <local> remote>. Once the port has been forwarded it can be 140 used in this object as the port of communication. 141 142 Attributes: 143 port: int, The host port to communicate through. 144 addr: str, The host address who is communicating to the device (usually 145 localhost). 146 client: file, The socket file used to communicate. 147 uid: int, The sl4a uid of this session. 148 conn: socket.Socket, The socket connection to the remote client. 149 """ 150 151 _SOCKET_TIMEOUT = 60 152 153 def __init__(self, port=PORT, addr=HOST, uid=UNKNOWN_UID): 154 """ 155 Args: 156 port: int, The port this client should connect to. 157 addr: str, The address this client should connect to. 158 uid: int, The uid of the session to join, or UNKNOWN_UID to start a 159 new session. 160 """ 161 self.port = port 162 self.addr = addr 163 self._lock = threading.Lock() 164 self.client = None # prevent close errors on connect failure 165 self.uid = uid 166 self.conn = None 167 168 def __del__(self): 169 self.close() 170 171 def _id_counter(self): 172 i = 0 173 while True: 174 yield i 175 i += 1 176 177 def open(self, cmd=Sl4aCommand.INIT): 178 """Opens a connection to the remote client. 179 180 Opens a connection to a remote client with sl4a. The connection will 181 error out if it takes longer than the connection_timeout time. Once 182 connected if the socket takes longer than _SOCKET_TIMEOUT to respond 183 the connection will be closed. 184 185 Args: 186 cmd: Sl4aCommand, The command to use for creating the connection. 187 188 Raises: 189 IOError: Raised when the socket times out from io error 190 socket.timeout: Raised when the socket waits to long for connection. 191 ProtocolError: Raised when there is an error in the protocol. 192 """ 193 self._counter = self._id_counter() 194 try: 195 self.conn = socket.create_connection((self.addr, self.port), 30) 196 except (socket.timeout): 197 logging.exception("Failed to create socket connection!") 198 raise 199 self.client = self.conn.makefile(mode="brw") 200 resp = self._cmd(cmd, self.uid) 201 if not resp: 202 raise ProtocolError( 203 ProtocolError.NO_RESPONSE_FROM_HANDSHAKE) 204 result = json.loads(str(resp, encoding="utf8")) 205 if result['status']: 206 self.uid = result['uid'] 207 else: 208 self.uid = UNKNOWN_UID 209 210 def close(self): 211 """Close the connection to the remote client.""" 212 if self.conn is not None: 213 self.conn.close() 214 self.conn = None 215 216 def _cmd(self, command, uid=None): 217 """Send a command to sl4a. 218 219 Given a command name, this will package the command and send it to 220 sl4a. 221 222 Args: 223 command: str, The name of the command to execute. 224 uid: int, the uid of the session to send the command to. 225 226 Returns: 227 The line that was written back. 228 """ 229 if not uid: 230 uid = self.uid 231 self.client.write(json.dumps({'cmd': command, 232 'uid': uid}).encode("utf8") + b'\n') 233 self.client.flush() 234 return self.client.readline() 235 236 def _rpc(self, method, *args): 237 """Sends an rpc to sl4a. 238 239 Sends an rpc call to sl4a using this clients connection. 240 241 Args: 242 method: str, The name of the method to execute. 243 args: any, The args to send to sl4a. 244 245 Returns: 246 The result of the rpc. 247 248 Raises: 249 ProtocolError: Something went wrong with the sl4a protocol. 250 ApiError: The rpc went through, however executed with errors. 251 """ 252 with self._lock: 253 apiid = next(self._counter) 254 data = {'id': apiid, 'method': method, 'params': args} 255 request = json.dumps(data) 256 self.client.write(request.encode("utf8") + b'\n') 257 self.client.flush() 258 response = self.client.readline() 259 if not response: 260 raise ProtocolError(ProtocolError.NO_RESPONSE_FROM_SERVER) 261 result = json.loads(str(response, encoding="utf8")) 262 if result['error']: 263 raise ApiError(result['error']) 264 if result['id'] != apiid: 265 raise ProtocolError(ProtocolError.MISMATCHED_API_ID) 266 return result['result'] 267 268 def __getattr__(self, name): 269 """Wrapper for python magic to turn method calls into RPC calls.""" 270 271 def rpc_call(*args): 272 return self._rpc(name, *args) 273 274 return rpc_call 275