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 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] %s' % (self.adb.serial,
62                                                  self.ports.client_port,
63                                                  self.uid, message)
64
65        self.log = logger.create_logger(_log_formatter)
66
67        self.ports = ports
68        self.set_timeout(rpc_client.SOCKET_TIMEOUT)
69
70    def open(self):
71        if self.uid != UNKNOWN_UID:
72            start_command = Sl4aConnectionCommand.CONTINUE
73        else:
74            start_command = Sl4aConnectionCommand.INIT
75
76        self._initiate_handshake(start_command)
77
78    def _initiate_handshake(self, start_command):
79        """Establishes a connection with the SL4A server.
80
81        Args:
82            start_command: The command to send. See Sl4aConnectionCommand.
83        """
84        try:
85            resp = self._cmd(start_command)
86        except socket.timeout as e:
87            self.log.error('Failed to open socket connection: %s', e)
88            raise
89        if not resp:
90            raise rpc_client.Sl4aProtocolError(
91                rpc_client.Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
92        result = json.loads(str(resp, encoding='utf8'))
93        if result['status']:
94            self.uid = result['uid']
95        else:
96            self.log.warning(
97                'UID not received for connection %s.' % self.ports)
98            self.uid = UNKNOWN_UID
99        self.log.debug('Created connection over: %s.' % self.ports)
100
101    def _cmd(self, command):
102        """Sends an session protocol command to SL4A to establish communication.
103
104        Args:
105            command: The name of the command to execute.
106
107        Returns:
108            The line that was written back.
109        """
110        self.send_request(json.dumps({'cmd': command, 'uid': self.uid}))
111        return self.get_response()
112
113    def get_new_ticket(self):
114        """Returns a ticket for a new request."""
115        with self._ticket_lock:
116            self._ticket_counter += 1
117            ticket = self._ticket_counter
118        return ticket
119
120    def set_timeout(self, timeout):
121        """Sets the socket's wait for response timeout."""
122        self._client_socket.settimeout(timeout)
123
124    def send_request(self, request):
125        """Sends a request over the connection."""
126        self._socket_file.write(request.encode('utf8') + b'\n')
127        self._socket_file.flush()
128        self.log.debug('Sent: ' + request)
129
130    def get_response(self):
131        """Returns the first response sent back to the client."""
132        data = self._socket_file.readline()
133        self.log.debug('Received: ' + data.decode('utf8', errors='replace'))
134        return data
135
136    def close(self):
137        """Closes the connection gracefully."""
138        self._client_socket.close()
139        self.adb.remove_tcp_forward(self.ports.forwarded_port)
140