|  | # 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 shutil | 
|  | import subprocess | 
|  | import tempfile | 
|  | import threading | 
|  | import uuid | 
|  | import select | 
|  |  | 
|  | 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 | 
|  | poll = select.poll() | 
|  | streams_by_fd = {} | 
|  | primary_fd = streams[0].fileno() | 
|  | for s in streams: | 
|  | poll.register(s.fileno(), select.POLLIN) | 
|  | streams_by_fd[s.fileno()] = s | 
|  |  | 
|  | try: | 
|  | while primary_fd != None: | 
|  | events = poll.poll(1) | 
|  | for fileno, event in events: | 
|  | if event & select.POLLIN: | 
|  | yield streams_by_fd[fileno].readline() | 
|  |  | 
|  | elif event & select.POLLHUP: | 
|  | poll.unregister(fileno) | 
|  | del streams_by_fd[fileno] | 
|  |  | 
|  | if fileno == primary_fd: | 
|  | primary_fd = None | 
|  | finally: | 
|  | for fd_to_cleanup, _ in streams_by_fd.iteritems(): | 
|  | poll.unregister(fd_to_cleanup) | 
|  |  | 
|  |  | 
|  | def DrainStreamToStdout(stream, quit_event): | 
|  | """Outputs the contents of |stream| until |quit_event| is set.""" | 
|  |  | 
|  | poll = select.poll() | 
|  | poll.register(stream.fileno(), select.POLLIN) | 
|  | try: | 
|  | while not quit_event.is_set(): | 
|  | events = poll.poll(1) | 
|  | for fileno, event in events: | 
|  | if event & select.POLLIN: | 
|  | print stream.readline().rstrip() | 
|  | elif event & select.POLLHUP: | 
|  | break | 
|  |  | 
|  | finally: | 
|  | poll.unregister(stream.fileno()) | 
|  |  | 
|  |  | 
|  | def RunPackage(output_dir, target, package_path, package_name, run_args, | 
|  | system_logging, symbolizer_config=None): | 
|  | """Copies the Fuchsia package at |package_path| to the target, | 
|  | executes it with |run_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. | 
|  | run_args: The arguments which will be passed to the Fuchsia process. | 
|  | system_logging: If true, connects a system log reader to the target. | 
|  | symbolizer_config: A newline delimited list of source files contained | 
|  | in the package. Omitting this parameter will disable | 
|  | symbolization. | 
|  |  | 
|  | Returns the exit code of the remote package process.""" | 
|  |  | 
|  |  | 
|  | system_logger = _AttachKernelLogReader(target) if system_logging else None | 
|  | package_copied = False | 
|  | 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() | 
|  |  | 
|  | logging.info('Copying package to target.') | 
|  | install_path = os.path.join('/data', os.path.basename(package_path)) | 
|  | target.PutFile(package_path, install_path) | 
|  | package_copied = True | 
|  |  | 
|  | logging.info('Installing 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)) | 
|  |  | 
|  | if system_logger: | 
|  | log_output_quit_event.set() | 
|  | log_output_thread.join(timeout=_JOIN_TIMEOUT_SECS) | 
|  |  | 
|  | logging.info('Running application.') | 
|  | command = ['run', package_name] + run_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 symbolizer_config: | 
|  | # Decorate the process output stream with the symbolizer. | 
|  | output = FilterStream(task_output, package_name, 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() | 
|  |  | 
|  | if package_copied: | 
|  | logging.info('Removing package source from device.') | 
|  | target.RunCommand(['rm', install_path]) | 
|  |  | 
|  |  | 
|  | return process.returncode |