blob: 6a22d6b039eb4a4567e07fc41c874b493c84eb42 [file]
"""Step class for managing a Fuchsia emulator instance using ffx emu."""
import logging
import os
import shutil
import socket
from unittest import mock
from slave import base_step
_STEP_DATA_FEMU_CAST = 'step_data_femu_cast'
_STEP_DATA_FEMU_HTTP = 'step_data_femu_http'
_STEP_DATA_FEMU_HTTPS = 'step_data_femu_https'
_STEP_DATA_FEMU_SSH = 'step_data_femu_ssh'
_STEP_DATA_FEMU_LOG = 'step_data_femu_log'
_STEP_DATA_PACKAGE_SERVER_LOG = 'step_data_package_server_log'
_FFX_LOG = 'ffx.log'
_FFX_DAEMON_LOG = 'ffx.daemon.log'
_SSH_CONFIG_TEMPLATE = '''
# Generated by Eureka CQ FuchsiaEmulatorStep using ffx emu.
# Ports are changed to an available TCP port assigned to QEMU.
Host *
Port {port}
StrictHostKeyChecking no
IdentityFile {auth_path}
ControlPersist 1m
ControlMaster auto
ControlPath /tmp/ssh-%r@%h:%p
ServerAliveInterval 1
ServerAliveCountMax 3
ConnectTimeout 5
'''
class FuchsiaEmulatorError(RuntimeError):
"""Errors relating to FEMU."""
class BaseFuchsiaEmulatorStep(base_step.BaseStep):
"""BaseFuchsiaEmulatorStep with common utilities."""
def __init__(self,
fuchsia_sdk_root,
fuchsia_workdir,
product='terminal.qemu-x64',
name='Unknown FEMU step',
**kwargs):
"""Creates a BaseFuchsiaEmulatorStep instance.
Args:
fuchsia_sdk_root: Path to the Fuchsia SDK.
fuchsia_workdir: Path to directory with Fuchsia artifacts.
product: Product image to run the emulator on.
name: Name of the step to pass to BaseStep.
kwargs: Remaining args to pass to BaseStep.
"""
base_step.BaseStep.__init__(self, name=name, **kwargs)
self._fuchsia_sdk_root = fuchsia_sdk_root
prod, board = product.split('.', 1)
self._product_bundle = os.path.join(fuchsia_sdk_root, 'images', prod, board)
# GN SDK scripts
self._ffx = os.path.join(self._fuchsia_sdk_root, 'sdk/tools/x64/ffx')
# Common Paths
self._fuchsia_workdir_path = fuchsia_workdir
def _dump_file_to_log(self, file_path):
"""Dump file to log.
Args:
file_path: Location of the file to add to the logger output.
"""
logging.info('%s log:', file_path)
with open(file_path) as file_fd:
logging.info(file_fd.read())
class FuchsiaEmulatorStartStep(BaseFuchsiaEmulatorStep):
"""Step for starting local Fuchsia assets on FEMU."""
def __init__(self, **kwargs):
"""Creates a FuchsiaEmulatorCreateStep instance.
Args:
kwargs: Passed to FuchsiaEmulatorBaseStep.
"""
BaseFuchsiaEmulatorStep.__init__(self, name="Start FEMU", **kwargs)
self.femu_log = None
self.package_server_log = None
self._ports_in_use = []
def _get_available_tcp_port(self, max_attempts=100):
"""Find a unique and open port.
Verify port is available by opening a listening socket on it.
Verify uniqueness by storing a list of ports returned, and ensuring no
duplicates in this list.
"""
# Search for a port that is open and verify it isn't
# already being used for another FEMU port.
attempts = 0
while attempts < max_attempts:
attempts += 1
# Look for an open port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("", 0))
port = sock.getsockname()[1]
sock.close()
# Verify port is not being used for another port
if port not in self._ports_in_use:
break
if attempts >= max_attempts:
raise FuchsiaEmulatorError('Failed to find open tcp port')
self._ports_in_use.append(port)
return port
def write_file(self, file, content):
with open(file, 'w') as f:
f.write(content)
def create_ssh_config(self):
"""Create SSH config for the test runner to connect to FEMU.
Assumes that FEMU has already been started.
"""
home_dir = os.path.expanduser('~')
ssh_dir = os.path.join(home_dir, '.ssh')
returncode, _, _ = self.exec_subprocess(['mkdir', '-p', ssh_dir])
if returncode != 0:
self._dump_file_to_log(self.femu_log)
self._dump_file_to_log(self.package_server_log)
raise FuchsiaEmulatorError('Failed to create ssh out dir')
ssh_config_path = os.path.join(home_dir, '.fuchsia', 'sshconfig')
ssh_config_content = _SSH_CONFIG_TEMPLATE.format(
port=self._ssh_port,
auth_path=os.path.join(ssh_dir, 'fuchsia_ed25519'))
self.write_file(ssh_config_path, ssh_config_content)
logging.info('ssh_config for chromium test at %s', ssh_config_path)
def start_femu(self):
"""Start Fuchsia Emulator process."""
self._ssh_port = self._get_available_tcp_port()
_, self.femu_log = self.get_new_tmp_file()
logging.info('femu log at %s', self.femu_log)
self.add_step_data(_STEP_DATA_FEMU_LOG, self.femu_log)
_, self.package_server_log = self.get_new_tmp_file()
logging.info('package server log at %s', self.package_server_log)
self.add_step_data(_STEP_DATA_PACKAGE_SERVER_LOG, self.package_server_log)
# HACK(b/290049007)
# For currently unknown reasons, there is an ffx daemon socket already
# present, but occassionally unresponsive. The emu start command will
# fail if it attempts to use this bad daemon. As a stop-gap measure
# to get Eureka CQ/CB back to green, we will force restart the daemon.
ffx_daemon_stop = [
self._ffx,
'-c', 'log.level=debug',
'-c', 'log.dir={}'.format(self._fuchsia_workdir_path),
'daemon',
'stop',
'-t', '5000',
]
self.exec_subprocess(ffx_daemon_stop, check_output=True)
# Start FEMU
femu_command = [
self._ffx,
'--timeout', '100',
'-c', 'log.level=debug',
'-c', 'log.dir={}'.format(self._fuchsia_workdir_path),
'emu',
'start',
'--startup-timeout', '100',
self._product_bundle,
'--log', self.femu_log,
'--port-map', f'ssh:{self._ssh_port}',
'--headless', # Run without gui on infra
]
self.exec_subprocess(femu_command, check_output=True)
def run(self):
"""Starts a local FEMU instance."""
self.start_femu()
# Copy SSH config needed for chromium test runners.
self.create_ssh_config()
# Output logs to give additional debugging information.
self._dump_file_to_log(self.femu_log)
# Store femu ports in step data for test steps.
self.add_step_data(_STEP_DATA_FEMU_SSH, self._ssh_port)
return True
class FuchsiaEmulatorTeardownStep(BaseFuchsiaEmulatorStep):
"""Step for cleaning up after the FuchsiaEmulatorStartStep."""
def __init__(self, **kwargs):
"""Creates a FuchsiaEmulatorTeardownStep instance."""
BaseFuchsiaEmulatorStep.__init__(self, name="Teardown FEMU", **kwargs)
def _copy_logs_to_gcs(self):
"""Copies FuchsiaEmulator related logs to the GCS directory"""
log_source_files_to_destinations = {
self.get_step_data(_STEP_DATA_FEMU_LOG): 'femu_system_log.txt',
self.get_step_data(
_STEP_DATA_PACKAGE_SERVER_LOG
): 'package_server_log.txt',
os.path.join(self._fuchsia_workdir_path, _FFX_LOG): 'ffx.log',
os.path.join(
self._fuchsia_workdir_path, _FFX_DAEMON_LOG
): 'ffx.daemon.log',
}
for src, dst in log_source_files_to_destinations.items():
if not os.path.isfile(src):
logging.warning('The expected log file "%s" does not exist!', src)
continue
gcs_upload_dst = os.path.join(self.get_gcs_dir(), dst)
shutil.copyfile(src, gcs_upload_dst)
def run(self):
"""Cleanup after FuchsiaEmulatorTeardownStep."""
# Kill FEMU
self._copy_logs_to_gcs()
femu_command = [self._ffx,
'emu',
'stop',
'--all',
]
# Don't check the return code. If emulator crashed, ffx emu will
# throw an error but it doesn't mean the kill action failed.
self.exec_subprocess(femu_command)
return True