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