• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
17JSON RPC interface to android scripting engine.
18"""
19
20from builtins import str
21
22import json
23import logging
24import os
25import socket
26import sys
27import threading
28import time
29
30from vts.utils.python.controllers import adb
31
32HOST = os.environ.get('SL4A_HOST_ADDRESS', None)
33PORT = os.environ.get('SL4A_HOST_PORT', 9999)
34DEFAULT_DEVICE_SIDE_PORT = 8080
35
36UNKNOWN_UID = -1
37
38MAX_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
45class Error(Exception):
46    pass
47
48
49class StartError(Error):
50    """Raised when sl4a is not able to be started."""
51
52
53class ApiError(Error):
54    """Raised when remote API reports an error."""
55
56
57class 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
64def 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
89def 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
107def 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
123class 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
133class 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