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