• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 socket
17import threading
18
19import errno
20
21from acts import logger
22from acts.controllers.adb_lib.error import AdbError
23from acts.controllers.sl4a_lib import event_dispatcher
24from acts.controllers.sl4a_lib import rpc_connection
25from acts.controllers.sl4a_lib import rpc_client
26from acts.controllers.sl4a_lib import sl4a_ports
27from acts.controllers.sl4a_lib.rpc_client import Sl4aStartError
28
29SOCKET_TIMEOUT = 60
30
31# The SL4A Session UID when a UID has not been received yet.
32UNKNOWN_UID = -1
33
34
35class Sl4aSession(object):
36    """An object that tracks the state of an SL4A Session.
37
38    Attributes:
39        _event_dispatcher: The EventDispatcher instance, if any, for this
40            session.
41        _terminate_lock: A lock that prevents race conditions for multiple
42            threads calling terminate()
43        _terminated: A bool that stores whether or not this session has been
44            terminated. Terminated sessions cannot be restarted.
45        adb: A reference to the AndroidDevice's AdbProxy.
46        log: The logger for this Sl4aSession
47        server_port: The SL4A server port this session is established on.
48        uid: The uid that corresponds the the SL4A Server's session id. This
49            value is only unique during the lifetime of the SL4A apk.
50    """
51
52    def __init__(self,
53                 adb,
54                 host_port,
55                 device_port,
56                 get_server_port_func,
57                 on_error_callback,
58                 forwarded_port=0,
59                 max_connections=None):
60        """Creates an SL4A Session.
61
62        Args:
63            adb: A reference to the adb proxy
64            get_server_port_func: A lambda (int) that returns the corrected
65                server port. The int passed in hints at which port to use, if
66                possible.
67            host_port: The port the host machine uses to connect to the SL4A
68                server for its first connection.
69            device_port: The SL4A server port to be used as a hint for which
70                SL4A server to connect to.
71            forwarded_port: The server port on host machine forwarded by adb
72                            from Android device to accept SL4A connection
73        """
74        self._event_dispatcher = None
75        self._terminate_lock = threading.Lock()
76        self._terminated = False
77        self.adb = adb
78
79        def _log_formatter(message):
80            return '[SL4A Session|%s|%s] %s' % (self.adb.serial, self.uid,
81                                                message)
82
83        self.log = logger.create_logger(_log_formatter)
84
85        self.forwarded_port = forwarded_port
86        self.server_port = device_port
87        self.uid = UNKNOWN_UID
88        self.obtain_server_port = get_server_port_func
89        self._on_error_callback = on_error_callback
90
91        connection_creator = self._rpc_connection_creator(host_port)
92        self.rpc_client = rpc_client.RpcClient(self.uid,
93                                               self.adb.serial,
94                                               self.diagnose_failure,
95                                               connection_creator,
96                                               max_connections=max_connections)
97
98    def _rpc_connection_creator(self, host_port):
99        def create_client(uid):
100            return self._create_rpc_connection(ports=sl4a_ports.Sl4aPorts(
101                host_port, self.forwarded_port, self.server_port),
102                                               uid=uid)
103
104        return create_client
105
106    @property
107    def is_alive(self):
108        return not self._terminated
109
110    def _create_forwarded_port(self, server_port, hinted_port=0):
111        """Creates a forwarded port to the specified server port.
112
113        Args:
114            server_port: (int) The port to forward to.
115            hinted_port: (int) The port to use for forwarding, if available.
116                         Otherwise, the chosen port will be random.
117        Returns:
118            The chosen forwarded port.
119
120        Raises AdbError if the version of ADB is too old, or the command fails.
121        """
122        if self.adb.get_version_number() < 37 and hinted_port == 0:
123            self.log.error(
124                'The current version of ADB does not automatically provide a '
125                'port to forward. Please upgrade ADB to version 1.0.37 or '
126                'higher.')
127            raise Sl4aStartError('Unable to forward a port to the device.')
128        else:
129            try:
130                return self.adb.tcp_forward(hinted_port, server_port)
131            except AdbError as e:
132                if 'cannot bind listener' in e.stderr:
133                    self.log.warning(
134                        'Unable to use %s to forward to device port %s due to: '
135                        '"%s". Attempting to choose a random port instead.' %
136                        (hinted_port, server_port, e.stderr))
137                    # Call this method again, but this time with no hinted port.
138                    return self._create_forwarded_port(server_port)
139                raise e
140
141    def _create_rpc_connection(self, ports=None, uid=UNKNOWN_UID):
142        """Creates an RPC Connection with the specified ports.
143
144        Args:
145            ports: A Sl4aPorts object or a tuple of (host/client_port,
146                   forwarded_port, device/server_port). If any of these are
147                   zero, the OS will determine their values during connection.
148
149                   Note that these ports are only suggestions. If they are not
150                   available, the a different port will be selected.
151            uid: The UID of the SL4A Session. To create a new session, use
152                 UNKNOWN_UID.
153        Returns:
154            An Sl4aClient.
155        """
156        if ports is None:
157            ports = sl4a_ports.Sl4aPorts(0, 0, 0)
158        # Open a new server if a server cannot be inferred.
159        ports.server_port = self.obtain_server_port(ports.server_port)
160        self.server_port = ports.server_port
161        # Forward the device port to the host.
162        ports.forwarded_port = self._create_forwarded_port(
163            ports.server_port, hinted_port=ports.forwarded_port)
164        client_socket, fd = self._create_client_side_connection(ports)
165        client = rpc_connection.RpcConnection(self.adb,
166                                              ports,
167                                              client_socket,
168                                              fd,
169                                              uid=uid)
170        client.open()
171        if uid == UNKNOWN_UID:
172            self.uid = client.uid
173        return client
174
175    def diagnose_failure(self, connection):
176        """Diagnoses any problems related to the SL4A session."""
177        self._on_error_callback(self, connection)
178
179    def get_event_dispatcher(self):
180        """Returns the EventDispatcher for this Sl4aSession."""
181        if self._event_dispatcher is None:
182            self._event_dispatcher = event_dispatcher.EventDispatcher(
183                self.adb.serial, self.rpc_client)
184        return self._event_dispatcher
185
186    def _create_client_side_connection(self, ports):
187        """Creates and connects the client socket to the forward device port.
188
189        Args:
190            ports: A Sl4aPorts object or a tuple of (host_port,
191            forwarded_port, device_port).
192
193        Returns:
194            A tuple of (socket, socket_file_descriptor).
195        """
196        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
197        client_socket.settimeout(SOCKET_TIMEOUT)
198        client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
199        if ports.client_port != 0:
200            try:
201                client_socket.bind((socket.gethostname(), ports.client_port))
202            except OSError as e:
203                # If the port is in use, log and ask for any open port.
204                if e.errno == errno.EADDRINUSE:
205                    self.log.warning('Port %s is already in use on the host. '
206                                     'Generating a random port.' %
207                                     ports.client_port)
208                    ports.client_port = 0
209                    return self._create_client_side_connection(ports)
210                raise
211
212        # Verify and obtain the port opened by SL4A.
213        try:
214            # Connect to the port that has been forwarded to the device.
215            client_socket.connect(('127.0.0.1', ports.forwarded_port))
216        except socket.timeout:
217            raise rpc_client.Sl4aConnectionError(
218                'SL4A has not connected over the specified port within the '
219                'timeout of %s seconds.' % SOCKET_TIMEOUT)
220        except socket.error as e:
221            # In extreme, unlikely cases, a socket error with
222            # errno.EADDRNOTAVAIL can be raised when a desired host_port is
223            # taken by a separate program between the bind and connect calls.
224            # Note that if host_port is set to zero, there is no bind before
225            # the connection is made, so this error will never be thrown.
226            if e.errno == errno.EADDRNOTAVAIL:
227                ports.client_port = 0
228                return self._create_client_side_connection(ports)
229            raise
230        ports.client_port = client_socket.getsockname()[1]
231        return client_socket, client_socket.makefile(mode='brw')
232
233    def terminate(self):
234        """Terminates the session.
235
236        The return of process execution is blocked on completion of all events
237        being processed by handlers in the Event Dispatcher.
238        """
239        with self._terminate_lock:
240            if not self._terminated:
241                self.log.debug('Terminating Session.')
242                try:
243                    self.rpc_client.closeSl4aSession()
244                except Exception as e:
245                    if "SL4A session has already been terminated" not in str(
246                            e):
247                        self.log.warning(e)
248                # Must be set after closeSl4aSession so the rpc_client does not
249                # think the session has closed.
250                self._terminated = True
251                if self._event_dispatcher:
252                    try:
253                        self._event_dispatcher.close()
254                    except Exception as e:
255                        self.log.warning(e)
256                try:
257                    self.rpc_client.terminate()
258                except Exception as e:
259                    self.log.warning(e)
260