• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 - The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""This module encapsulates emulator (goldfish) console.
16
17Reference: https://developer.android.com/studio/run/emulator-console
18"""
19
20import logging
21import socket
22import subprocess
23
24from acloud import errors
25from acloud.internal.lib import utils
26
27logger = logging.getLogger(__name__)
28_DEFAULT_SOCKET_TIMEOUT_SECS = 20
29_LOCALHOST_IP_ADDRESS = "127.0.0.1"
30
31
32class RemoteEmulatorConsole:
33    """Connection to a remote emulator console through SSH tunnel.
34
35    Attributes:
36        local_port: The local port of the SSH tunnel.
37        socket: The TCP connection to the console.
38        timeout_secs: The timeout for the TCP connection.
39    """
40
41    def __init__(self, ip_addr, port, ssh_user, ssh_private_key_path,
42                 ssh_extra_args, timeout_secs=_DEFAULT_SOCKET_TIMEOUT_SECS):
43        """Create a SSH tunnel and a TCP connection to an emulator console.
44
45        Args:
46            ip_addr: A string, the IP address of the emulator console.
47            port: An integer, the port of the emulator console.
48            ssh_user: A string, the user name for SSH.
49            ssh_private_key_path: A string, the private key path for SSH.
50            ssh_extra_args: A string, the extra arguments for SSH.
51            timeout_secs: An integer, the timeout for the TCP connection.
52
53        Raises:
54            errors.DeviceConnectionError if the connection fails.
55        """
56        logger.debug("Connect to %s:%d", ip_addr, port)
57        self._local_port = None
58        self._socket = None
59        self._timeout_secs = timeout_secs
60        try:
61            self._local_port = utils.PickFreePort()
62            utils.EstablishSshTunnel(
63                ip_addr,
64                ssh_private_key_path,
65                ssh_user,
66                [(self._local_port, port)],
67                ssh_extra_args)
68        except (OSError, subprocess.CalledProcessError) as e:
69            raise errors.DeviceConnectionError(
70                "Cannot create SSH tunnel to %s:%d." % (ip_addr, port)) from e
71
72        try:
73            self._socket = socket.create_connection(
74                (_LOCALHOST_IP_ADDRESS, self._local_port), timeout_secs)
75            self._socket.settimeout(timeout_secs)
76        except OSError as e:
77            if self._socket:
78                self._socket.close()
79            utils.ReleasePort(self._local_port)
80            raise errors.DeviceConnectionError(
81                "Cannot connect to %s:%d." % (ip_addr, port)) from e
82
83    def __enter__(self):
84        return self
85
86    def __exit__(self, exc_type, msg, trackback):
87        self._socket.close()
88        utils.ReleasePort(self._local_port)
89
90    def Reconnect(self):
91        """Retain the SSH tunnel and reconnect the console socket.
92
93        Raises:
94            errors.DeviceConnectionError if the connection fails.
95        """
96        logger.debug("Reconnect to %s:%d",
97                     _LOCALHOST_IP_ADDRESS, self._local_port)
98        try:
99            self._socket.close()
100            self._socket = socket.create_connection(
101                (_LOCALHOST_IP_ADDRESS, self._local_port), self._timeout_secs)
102            self._socket.settimeout(self._timeout_secs)
103        except OSError as e:
104            raise errors.DeviceConnectionError(
105                "Fail to reconnect to %s:%d" %
106                (_LOCALHOST_IP_ADDRESS, self._local_port)) from e
107
108    def Send(self, command):
109        """Send a command to the console.
110
111        Args:
112            command: A string, the command without newline character.
113
114        Raises:
115            errors.DeviceConnectionError if the socket fails.
116        """
117        logger.debug("Emu command: %s", command)
118        try:
119            self._socket.send(command.encode() + b"\n")
120        except OSError as e:
121            raise errors.DeviceConnectionError(
122                "Fail to send to %s:%d." %
123                (_LOCALHOST_IP_ADDRESS, self._local_port)) from e
124
125    def Recv(self, expected_substring, buffer_size=128):
126        """Receive from the console until getting the expected substring.
127
128        Args:
129            expected_substring: The expected substring in the received data.
130            buffer_size: The buffer size in bytes for each recv call.
131
132        Returns:
133            The received data as a string.
134
135        Raises:
136            errors.DeviceConnectionError if the received data does not contain
137            the expected substring.
138        """
139        expected_data = expected_substring.encode()
140        data = bytearray()
141        while True:
142            try:
143                new_data = self._socket.recv(buffer_size)
144            except OSError as e:
145                raise errors.DeviceConnectionError(
146                    "Fail to receive from %s:%d." %
147                    (_LOCALHOST_IP_ADDRESS, self._local_port)) from e
148            if not new_data:
149                raise errors.DeviceConnectionError(
150                    "Connection to %s:%d is closed." %
151                    (_LOCALHOST_IP_ADDRESS, self._local_port))
152
153            logger.debug("Emu output: %s", new_data)
154            data.extend(new_data)
155            if expected_data in data:
156                break
157        return data.decode()
158
159    def Ping(self):
160        """Send ping command.
161
162        Returns:
163            Whether the console is active.
164        """
165        try:
166            self.Send("ping")
167            self.Recv("I am alive!")
168        except errors.DeviceConnectionError as e:
169            logger.debug("Fail to ping console: %s", str(e))
170            return False
171        return True
172
173    def Kill(self):
174        """Send kill command.
175
176        Raises:
177            errors.DeviceConnectionError if the console is not killed.
178        """
179        self.Send("kill")
180        self.Recv("bye bye")
181