blob: 077ff28defd31f91faecfde535caed3488a2c20b [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Contains a helper function for deploying and executing a packaged
executable on a Target."""
import common
import json
import logging
import multiprocessing
import os
import select
import shutil
import subprocess
import sys
import tempfile
import threading
import uuid
from symbolizer import FilterStream
FAR = os.path.join(common.SDK_ROOT, 'tools', 'far')
PM = os.path.join(common.SDK_ROOT, 'tools', 'pm')
# Amount of time to wait for the termination of the system log output thread.
_JOIN_TIMEOUT_SECS = 5
def _AttachKernelLogReader(target):
"""Attaches a kernel log reader as a long-running SSH task."""
logging.info('Attaching kernel logger.')
return target.RunCommandPiped(['dlog', '-f'], stdin=open(os.devnull, 'r'),
stdout=subprocess.PIPE)
def _ReadMergedLines(streams):
"""Creates a generator which merges the buffered line output from |streams|.
The generator is terminated when the primary (first in sequence) stream
signals EOF. Absolute output ordering is not guaranteed."""
assert len(streams) > 0
streams_by_fd = {}
primary_fd = streams[0].fileno()
for s in streams:
streams_by_fd[s.fileno()] = s
while primary_fd != None:
rlist, _, _ = select.select(streams_by_fd, [], [], 0.1)
for fileno in rlist:
line = streams_by_fd[fileno].readline()
if line:
yield line
elif fileno == primary_fd:
primary_fd = None
else:
del streams_by_fd[fileno]
def _DrainStreamToStdout(stream, quit_event):
"""Outputs the contents of |stream| until |quit_event| is set."""
while not quit_event.is_set():
rlist, _, _ = select.select([ stream ], [], [], 0.1)
if rlist:
line = rlist[0].readline()
if not line:
return
print line.rstrip()
class RunPackageArgs:
"""RunPackage() configuration arguments structure.
install_only: If set, skips the package execution step.
symbolizer_config: A newline delimited list of source files contained
in the package. Omitting this parameter will disable symbolization.
system_logging: If set, connects a system log reader to the target.
target_staging_path: Path to which package FARs will be staged, during
installation. Defaults to staging into '/data'.
"""
def __init__(self):
self.install_only = False
self.symbolizer_config = None
self.system_logging = False
self.target_staging_path = '/data'
@staticmethod
def FromCommonArgs(args):
run_package_args = RunPackageArgs()
run_package_args.install_only = args.install_only
run_package_args.symbolizer_config = args.package_manifest
run_package_args.system_logging = args.include_system_logs
run_package_args.target_staging_path = args.target_staging_path
return run_package_args
def RunPackage(output_dir, target, package_path, package_name, package_deps,
package_args, args):
"""Copies the Fuchsia package at |package_path| to the target,
executes it with |package_args|, and symbolizes its output.
output_dir: The path containing the build output files.
target: The deployment Target object that will run the package.
package_path: The path to the .far package file.
package_name: The name of app specified by package metadata.
package_args: The arguments which will be passed to the Fuchsia process.
args: Structure of arguments to configure how the package will be run.
Returns the exit code of the remote package process."""
system_logger = (
_AttachKernelLogReader(target) if args.system_logging else None)
try:
if system_logger:
# Spin up a thread to asynchronously dump the system log to stdout
# for easier diagnoses of early, pre-execution failures.
log_output_quit_event = multiprocessing.Event()
log_output_thread = threading.Thread(
target=lambda: _DrainStreamToStdout(system_logger.stdout,
log_output_quit_event))
log_output_thread.daemon = True
log_output_thread.start()
for next_package_path in ([package_path] + package_deps):
logging.info('Installing ' + os.path.basename(next_package_path) + '.')
# Copy the package archive.
install_path = os.path.join(args.target_staging_path,
os.path.basename(next_package_path))
target.PutFile(next_package_path, install_path)
# Install the package.
p = target.RunCommandPiped(['pm', 'install', install_path],
stderr=subprocess.PIPE)
output = p.stderr.readlines()
p.wait()
if p.returncode != 0:
# Don't error out if the package already exists on the device.
if len(output) != 1 or 'ErrAlreadyExists' not in output[0]:
raise Exception('Error while installing: %s' % '\n'.join(output))
# Clean up the package archive.
target.RunCommand(['rm', install_path])
if system_logger:
log_output_quit_event.set()
log_output_thread.join(timeout=_JOIN_TIMEOUT_SECS)
if args.install_only:
logging.info('Installation complete.')
return
logging.info('Running application.')
command = ['run', package_name] + package_args
process = target.RunCommandPiped(command,
stdin=open(os.devnull, 'r'),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
if system_logger:
task_output = _ReadMergedLines([process.stdout, system_logger.stdout])
else:
task_output = process.stdout
if args.symbolizer_config:
# Decorate the process output stream with the symbolizer.
output = FilterStream(task_output, package_name, args.symbolizer_config,
output_dir)
else:
logging.warn('Symbolization is DISABLED.')
output = process.stdout
for next_line in output:
print next_line.rstrip()
process.wait()
if process.returncode == 0:
logging.info('Process exited normally with status code 0.')
else:
# The test runner returns an error status code if *any* tests fail,
# so we should proceed anyway.
logging.warning('Process exited with status code %d.' %
process.returncode)
finally:
if system_logger:
logging.info('Terminating kernel log reader.')
log_output_quit_event.set()
log_output_thread.join()
system_logger.kill()
return process.returncode