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 json
17import socket
18import threading
19
20from acts import logger
21from acts.controllers.sl4a_lib import rpc_client
22
23# The Session UID when a UID has not been received yet.
24UNKNOWN_UID = -1
25
26
27class Sl4aConnectionCommand(object):
28    """Commands that can be invoked on the sl4a client.
29
30    INIT: Initializes a new sessions in sl4a.
31    CONTINUE: Creates a connection.
32    """
33    INIT = 'initiate'
34    CONTINUE = 'continue'
35
36
37class RpcConnection(object):
38    """A single RPC Connection thread.
39
40    Attributes:
41        _client_socket: The socket this connection uses.
42        _socket_file: The file created over the _client_socket.
43        _ticket_counter: The counter storing the current ticket number.
44        _ticket_lock: A lock on the ticket counter to prevent ticket collisions.
45        adb: A reference to the AdbProxy of the AndroidDevice. Used for logging.
46        log: The logger for this RPC Client.
47        ports: The Sl4aPorts object that stores the ports this connection uses.
48        uid: The SL4A session ID.
49    """
50
51    def __init__(self, adb, ports, client_socket, socket_fd, uid=UNKNOWN_UID):
52        self._client_socket = client_socket
53        self._socket_file = socket_fd
54        self._ticket_counter = 0
55        self._ticket_lock = threading.Lock()
56        self.adb = adb
57        self.uid = uid
58
59        def _log_formatter(message):
60            """Defines the formatting used in the logger."""
61            return '[SL4A Client|%s|%s] %s' % (self.adb.serial, self.uid,
62                                               message)
63
64        self.log = logger.create_logger(_log_formatter)
65
66        self.ports = ports
67        self.set_timeout(rpc_client.SOCKET_TIMEOUT)
68
69    def open(self):
70        if self.uid != UNKNOWN_UID:
71            start_command = Sl4aConnectionCommand.CONTINUE
72        else:
73            start_command = Sl4aConnectionCommand.INIT
74
75        self._initiate_handshake(start_command)
76
77    def _initiate_handshake(self, start_command):
78        """Establishes a connection with the SL4A server.
79
80        Args:
81            start_command: The command to send. See Sl4aConnectionCommand.
82        """
83        try:
84            resp = self._cmd(start_command)
85        except socket.timeout as e:
86            self.log.error('Failed to open socket connection: %s', e)
87            raise
88        if not resp:
89            raise rpc_client.Sl4aProtocolError(
90                rpc_client.Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
91        result = json.loads(str(resp, encoding='utf8'))
92        if result['status']:
93            self.uid = result['uid']
94        else:
95            self.log.warning(
96                'UID not received for connection %s.' % self.ports)
97            self.uid = UNKNOWN_UID
98        self.log.debug('Created connection over: %s.' % self.ports)
99
100    def _cmd(self, command):
101        """Sends an session protocol command to SL4A to establish communication.
102
103        Args:
104            command: The name of the command to execute.
105
106        Returns:
107            The line that was written back.
108        """
109        self.send_request(json.dumps({'cmd': command, 'uid': self.uid}))
110        return self.get_response()
111
112    def get_new_ticket(self):
113        """Returns a ticket for a new request."""
114        with self._ticket_lock:
115            self._ticket_counter += 1
116            ticket = self._ticket_counter
117        return ticket
118
119    def set_timeout(self, timeout):
120        """Sets the socket's wait for response timeout."""
121        self._client_socket.settimeout(timeout)
122
123    def send_request(self, request):
124        """Sends a request over the connection."""
125        self._socket_file.write(request.encode('utf8') + b'\n')
126        self._socket_file.flush()
127
128    def get_response(self):
129        """Returns the first response sent back to the client."""
130        data = self._socket_file.readline()
131        return data
132
133    def close(self):
134        """Closes the connection gracefully."""
135        self._client_socket.close()
136        self.adb.remove_tcp_forward(self.ports.forwarded_port)
137