| # 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 |