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