# Copyright 2021 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This module encapsulates emulator (goldfish) console. Reference: https://developer.android.com/studio/run/emulator-console """ import logging import socket import subprocess from acloud import errors from acloud.internal.lib import utils logger = logging.getLogger(__name__) _DEFAULT_SOCKET_TIMEOUT_SECS = 20 _LOCALHOST_IP_ADDRESS = "127.0.0.1" class RemoteEmulatorConsole: """Connection to a remote emulator console through SSH tunnel. Attributes: local_port: The local port of the SSH tunnel. socket: The TCP connection to the console. timeout_secs: The timeout for the TCP connection. """ def __init__(self, ip_addr, port, ssh_user, ssh_private_key_path, ssh_extra_args, timeout_secs=_DEFAULT_SOCKET_TIMEOUT_SECS): """Create a SSH tunnel and a TCP connection to an emulator console. Args: ip_addr: A string, the IP address of the emulator console. port: An integer, the port of the emulator console. ssh_user: A string, the user name for SSH. ssh_private_key_path: A string, the private key path for SSH. ssh_extra_args: A string, the extra arguments for SSH. timeout_secs: An integer, the timeout for the TCP connection. Raises: errors.DeviceConnectionError if the connection fails. """ logger.debug("Connect to %s:%d", ip_addr, port) self._local_port = None self._socket = None self._timeout_secs = timeout_secs try: self._local_port = utils.PickFreePort() utils.EstablishSshTunnel( ip_addr, ssh_private_key_path, ssh_user, [(self._local_port, port)], ssh_extra_args) except (OSError, subprocess.CalledProcessError) as e: raise errors.DeviceConnectionError( "Cannot create SSH tunnel to %s:%d." % (ip_addr, port)) from e try: self._socket = socket.create_connection( (_LOCALHOST_IP_ADDRESS, self._local_port), timeout_secs) self._socket.settimeout(timeout_secs) except OSError as e: if self._socket: self._socket.close() utils.ReleasePort(self._local_port) raise errors.DeviceConnectionError( "Cannot connect to %s:%d." % (ip_addr, port)) from e def __enter__(self): return self def __exit__(self, exc_type, msg, trackback): self._socket.close() utils.ReleasePort(self._local_port) def Reconnect(self): """Retain the SSH tunnel and reconnect the console socket. Raises: errors.DeviceConnectionError if the connection fails. """ logger.debug("Reconnect to %s:%d", _LOCALHOST_IP_ADDRESS, self._local_port) try: self._socket.close() self._socket = socket.create_connection( (_LOCALHOST_IP_ADDRESS, self._local_port), self._timeout_secs) self._socket.settimeout(self._timeout_secs) except OSError as e: raise errors.DeviceConnectionError( "Fail to reconnect to %s:%d" % (_LOCALHOST_IP_ADDRESS, self._local_port)) from e def Send(self, command): """Send a command to the console. Args: command: A string, the command without newline character. Raises: errors.DeviceConnectionError if the socket fails. """ logger.debug("Emu command: %s", command) try: self._socket.send(command.encode() + b"\n") except OSError as e: raise errors.DeviceConnectionError( "Fail to send to %s:%d." % (_LOCALHOST_IP_ADDRESS, self._local_port)) from e def Recv(self, expected_substring, buffer_size=128): """Receive from the console until getting the expected substring. Args: expected_substring: The expected substring in the received data. buffer_size: The buffer size in bytes for each recv call. Returns: The received data as a string. Raises: errors.DeviceConnectionError if the received data does not contain the expected substring. """ expected_data = expected_substring.encode() data = bytearray() while True: try: new_data = self._socket.recv(buffer_size) except OSError as e: raise errors.DeviceConnectionError( "Fail to receive from %s:%d." % (_LOCALHOST_IP_ADDRESS, self._local_port)) from e if not new_data: raise errors.DeviceConnectionError( "Connection to %s:%d is closed." % (_LOCALHOST_IP_ADDRESS, self._local_port)) logger.debug("Emu output: %s", new_data) data.extend(new_data) if expected_data in data: break return data.decode() def Ping(self): """Send ping command. Returns: Whether the console is active. """ try: self.Send("ping") self.Recv("I am alive!") except errors.DeviceConnectionError as e: logger.debug("Fail to ping console: %s", str(e)) return False return True def Kill(self): """Send kill command. Raises: errors.DeviceConnectionError if the console is not killed. """ self.Send("kill") self.Recv("bye bye")