Home | History | Annotate | Download | only in controllers
      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