| # Copyright 2017 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. |
| |
| """Event subprocess module. |
| |
| Event subprocesses are subprocesses that print events to stdout. |
| |
| Each event is one line of ASCII text with a terminating newline |
| character. The event is identified with one of the preset strings in |
| Event. The event string may be followed with a single space and a |
| message, on the same line. The interpretation of the message is up to |
| the event handler. |
| |
| run_event_command() starts such a process with a synchronous event |
| handler. |
| """ |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import logging |
| |
| import enum |
| import subprocess32 |
| from subprocess32 import PIPE |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| class Event(enum.Enum): |
| """Status change event enum |
| |
| Members of this enum represent all possible status change events |
| that can be emitted by an event command and that need to be handled |
| by the caller. |
| |
| The value of enum members must be a string, which is printed by |
| itself on a line to signal the event. |
| |
| This should be backward compatible with all versions of lucifer, |
| which lives in the infra/lucifer repository. |
| |
| TODO(crbug.com/748234): Events starting with X are temporary to |
| support gradual lucifer rollout. |
| |
| https://chromium.googlesource.com/chromiumos/infra/lucifer |
| """ |
| # Job status |
| STARTING = 'starting' |
| RUNNING = 'running' |
| GATHERING = 'gathering' |
| PARSING = 'parsing' |
| ABORTED = 'aborted' |
| COMPLETED = 'completed' |
| |
| # Test status |
| TEST_PASSED = 'test_passed' |
| TEST_FAILED = 'test_failed' |
| |
| # Host status |
| HOST_RUNNING = 'host_running' |
| HOST_READY = 'host_ready' |
| HOST_NEEDS_CLEANUP = 'host_needs_cleanup' |
| HOST_NEEDS_RESET = 'host_needs_reset' |
| |
| # Temporary |
| X_TESTS_DONE = 'x_tests_done' # Only for GATHERING |
| |
| |
| def run_event_command(event_handler, args): |
| """Run a command that emits events. |
| |
| Events printed by the command to stdout will be handled by |
| event_handler synchronously. Exceptions raised by event_handler |
| will not be caught. If an exception escapes, the child process's |
| standard file descriptors are closed and the process is waited for. |
| The event command should terminate if this happens. |
| |
| event_handler is called to handle each event. Malformed events |
| emitted by the command will be logged and discarded. The |
| event_handler should take two positional arguments: an Event |
| instance and a message string. |
| |
| @param event_handler: event handler. |
| @param args: passed to subprocess.Popen. |
| @param returns: exit status of command. |
| """ |
| logger.debug('Starting event command with %r', args) |
| with subprocess32.Popen(args, stdout=PIPE, close_fds=True) as proc: |
| logger.debug('Event command child pid is %d', proc.pid) |
| _handle_subprocess_events(event_handler, proc) |
| logger.debug('Event command child with pid %d exited with %d', |
| proc.pid, proc.returncode) |
| return proc.returncode |
| |
| |
| def _handle_subprocess_events(event_handler, proc): |
| """Handle a subprocess that emits events. |
| |
| Events printed by the subprocess will be handled by event_handler. |
| |
| @param event_handler: callable that takes an Event instance. |
| @param proc: Popen instance. |
| """ |
| while True: |
| logger.debug('Reading subprocess stdout') |
| line = proc.stdout.readline() |
| if not line: |
| break |
| _handle_output_line(event_handler, line) |
| |
| |
| def _handle_output_line(event_handler, line): |
| """Handle a line of output from an event subprocess. |
| |
| @param event_handler: callable that takes a StatusChangeEvent. |
| @param line: line of output. |
| """ |
| event_str, _, message = line.rstrip().partition(' ') |
| try: |
| event = Event(event_str) |
| except ValueError: |
| logger.warning('Invalid output %r received', line) |
| return |
| event_handler(event, message) |