blob: edb1377be3ab911a1520dc8c91fcb64f10dad2ba [file] [log] [blame] [edit]
"""Utils for running a desktop cast_shell instance."""
from __future__ import absolute_import
import json
import logging
import os
import shutil
import socket
import sys
import time
# The step property key that stores the cast_shell executable path.
CAST_SHELL_EXE_PATH = 'cast_shell_exe_path'
_AVAILABILITY_RETRY_WAIT_TIME_SECS = 10
_AVAILABILITY_WAIT_TIME_SECS = 80
_CAST_V2_DEBUG_APP_ID = 'C566BECD'
_CONNECTION_NAMESPACE = 'urn:x-cast:com.google.cast.tp.connection'
_SETUP_API_ROOT = 'http://{device_ip}:8008/setup/{api}'
_OZONE_PLATFORM_CAST = 'cast'
_OZONE_PLATFORM_HEADLESS = 'headless' # Used for chirp_shell.
# Default cast_shell parameters
_CAST_V2_PORT = 8009
_MULTIZONE_PORT = 10001
_REMOTE_DUBIGGING_PORT = 9222
_MDNS_MULTICAST_PORT = 5353
_GCM_SUB_PATH = 'gcm_cast_cq'
_DUMP_LOGCAT_MSG = ('dump of cast_shell logs: \n'
'*** BEGIN CAST_SHELL ***\n'
'%s\n'
'*** END CAST_SHELL ***\n')
class CastShellError(Exception):
"""An error while trying to use cast_shell."""
class CastShellTimeoutError(Exception):
"""A timeout error while trying to use cast_shell."""
def start_cast_shell(executor,
test_project_path,
cast_shell_path,
tmp_dir,
enable_cma_media_pipeline=False,
enable_ccs_transport_v2=False,
use_headless_ozone_platform=False,
launch_utility_process=False):
"""Starts a local x86 cast shell.
Note: if multiple cast_shell instances are to be started, they must
all have unique ports for all the keyword ports specified below and unique
HOME and TMPDIR directories.
Args:
executor: Executor to run the subprocess
test_project_path: Path to the 'test' project. This will be appened to the
sys.path if it is not already there.
cast_shell_path: Path the to cast_shell executable
tmp_dir: A temporary dir to use as HOME and TMPDIR. If there are multiple
cast_shell instances, this should be unique across all instances.
enable_cma_media_pipeline: If True, pass in --enable-cma-media-pipeline
enable_ccs_transport_v2: If True, pass in --enable-ccs-transport-v2
use_headless_ozone_platform: If True, use "headless" as --ozone-platform
(required for assistant-enabled cast_shells); otherwise, uses "cast".
launch_utility_process: If True, launches the utility process (required to
start the assistant debug HTTP server at port 8007).
Returns:
A tuple of (<started subprocess>,
<file descriptor of cast_shell output file>,
<absolute path of cast_shell output file>).
These can be passed to upload_cast_shell_logs() below to store the log
files in GCS.
Raises:
CastShellError: if cast_shell does not start correctly.
CastShellTimeoutError: if cast_shell takes too long to become available.
"""
if test_project_path not in sys.path:
sys.path.append(test_project_path)
if use_headless_ozone_platform:
ozone_platform = _OZONE_PLATFORM_HEADLESS
else:
ozone_platform = _OZONE_PLATFORM_CAST
command = [
cast_shell_path,
'--no-sandbox',
'--disable-gpu',
'--ignore-gpu-blacklist',
'--ozone-platform={}'.format(ozone_platform),
'--port-for-cast-v2={}'.format(_CAST_V2_PORT),
'--port-for-multizone={}'.format(_MULTIZONE_PORT),
'--remote-debugging-port={}'.format(_REMOTE_DUBIGGING_PORT),
'--port-for-mdns-multicast={}'.format(_MDNS_MULTICAST_PORT),
'--gcm-sub-path="{}"'.format(_GCM_SUB_PATH),
'--disable-reboot-on-config-update',
]
if launch_utility_process:
# max-output-volume-dba1m is required when --launch-utility-process
# is specified.
command.extend(['--launch-utility-process', '--max-output-volume-dba1m=88'])
if enable_cma_media_pipeline:
command.append('--enable-cma-media-pipeline')
if enable_ccs_transport_v2:
command.append('--enable-ccs-transport-v2')
# HOME and TMPDIR should be unique across any running cast_shell instances.
cast_shell_env = {'HOME': tmp_dir, 'TMPDIR': tmp_dir}
try:
subprocess, log_file_descriptor, log_file_path = (
executor.exec_non_blocking_subprocess(command, env=cast_shell_env))
if subprocess.returncode is not None:
raise CastShellError('cast_shell startup failed')
if not _await_cast_shell_availability():
raise CastShellTimeoutError(
'cast_shell took too long to become available.')
except:
logging.exception('Unable to start Cast Shell.')
_dump_cast_shell_logs(log_file_descriptor, log_file_path)
raise
logging.info('cast_shell started successfully.')
return subprocess, log_file_descriptor, log_file_path
def _dump_cast_shell_logs(log_file_descriptor, log_file_path):
"""Dump the cast_shell logcat to the logger."""
os.fsync(log_file_descriptor)
os.close(log_file_descriptor)
with open(log_file_path, 'r') as log_file:
logging.info(_DUMP_LOGCAT_MSG, log_file.read())
def upload_cast_shell_logs(log_file_descriptor, log_file_path, gcs_root_dir,
sub_dir, upload_file_name):
"""Move the process log file into GCS folder so that it's uploaded.
Args:
log_file_descriptor: File descriptor of log file to be copied. This should
be the descriptor returned by start_cast_shell() above.
log_file_path: Absolute path of the log file to be copied. This should be
the path returned by start_cast_shell() above.
gcs_root_dir: The GCS directory.
sub_dir: The sub directory within GCS to upload to.
upload_file_name: A string; log file will be renamed and uploaded with this
name.
"""
output_dir = os.path.join(gcs_root_dir, sub_dir)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
os.fsync(log_file_descriptor)
shutil.copy(log_file_path, os.path.join(output_dir, upload_file_name))
logging.info(
'cast_shell log will be avilable on GCS in the gs://cast-cq bucket under '
'%s for this CL. (See the end of stdio of the "shell" step for the '
'specific path. It should be similar to: '
'gs://cast-cq/eureka-interal/project/branch/issue/patchset',
os.path.join(sub_dir, upload_file_name))
def _get_eureka_info(executor, ip_address='localhost', params=None):
"""Returns the parsed eureka info.
The //chromecast/internal/setup/eureka_info.h describes the various fields
that will be available.
This assumes that a cast_shell instance is available at |ip_address| (such
as from running start_cast_shell above).
Args:
executor: Executor to run the subprocess
ip_address: IP address (as a string) of the device to get the info from.
(Defaults to 'localhost')
params: A list of parameters (as strings) to get from euruka_info. This must
be specified and cannot be an empty list. e.g. params=['name',
'wifi.ssid', 'setup.stats.num_obtain_ip'] will
return {'name': 'devicename',
'wifi': {'ssid': 'wifi-ssid-value'},
'setup': {'stats': {'num_obtain_ip': 0}}}
Returns:
A dict with the eureka_info information on success, or an empty dict on
failure.
"""
api_endpoint = _SETUP_API_ROOT.format(
device_ip=ip_address,
api='eureka_info?params={}'.format(','.join(params)))
logging.info('Making Request to API Endpoint: %s', api_endpoint)
returncode, json_result, _ = executor.exec_subprocess(['curl', api_endpoint],
quiet=True)
if returncode == 0:
return json.loads(json_result)
return {}
def get_device_name(executor, ip_address='localhost'):
"""Returns the name of the device at |ip_address|."""
return _get_eureka_info(executor, ip_address, ['name']).get('name')
def _await_cast_shell_availability(max_wait_time=_AVAILABILITY_WAIT_TIME_SECS):
"""Wait's until max_wait_time seconds for cast shell availability.
Args:
max_wait_time: Integer of wait time in seconds.
Returns:
True if cast shell is up and running.
"""
remaining_time = max_wait_time
while remaining_time >= 0:
time.sleep(_AVAILABILITY_RETRY_WAIT_TIME_SECS)
remaining_time -= _AVAILABILITY_RETRY_WAIT_TIME_SECS
try:
if _connect_cast_shell():
return True
except (socket.error, AssertionError):
logging.exception('Cast shell is not ready, will retry for %d seconds',
remaining_time)
return False
def _connect_cast_shell():
"""Returns True if able to to connect and send a message to cast shell.
Returns:
True if able to to connect and send a message to cast shell.
Raises:
Socket.error: if not able to connect to cast shell.
AssertionError: if not able to send message to cast shell.
"""
# Assumes that test/ is on the PYTHONPATH
transport = __import__(
'cast.tools.transport',
fromlist=[
'application', 'application_connection', 'receiver', 'transport'
])
receiver = transport.receiver.Receiver('receiver-0', 'localhost')
app = transport.application.Application(receiver, _CAST_V2_DEBUG_APP_ID)
try:
with (transport.application_connection.ApplicationConnection(
app, 'sender-1')) as app_conn:
app_conn.ConnectToReceiver()
message = transport.transport.BuildMessage(
'sender-1', 'receiver-0', _CONNECTION_NAMESPACE, b'', binary=True)
app_conn.Send(message)
app_conn.Status()
except transport.socket_helper.SocketError as e:
raise socket.error(socket.error(e), sys.exc_info()[2])
return True