1# Copyright 2017 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Event subprocess module. 6 7Event subprocesses are subprocesses that print events to stdout. 8 9Each event is one line of ASCII text with a terminating newline 10character. The event is identified with one of the preset strings in 11Event. The event string may be followed with a single space and a 12message, on the same line. The interpretation of the message is up to 13the event handler. 14 15run_event_command() starts such a process with a synchronous event 16handler. 17""" 18 19from __future__ import absolute_import 20from __future__ import division 21from __future__ import print_function 22 23import logging 24 25import enum 26import subprocess32 27from subprocess32 import PIPE 28 29logger = logging.getLogger(__name__) 30 31 32class Event(enum.Enum): 33 """Status change event enum 34 35 Members of this enum represent all possible status change events 36 that can be emitted by an event command and that need to be handled 37 by the caller. 38 39 The value of enum members must be a string, which is printed by 40 itself on a line to signal the event. 41 42 This should be backward compatible with all versions of lucifer, 43 which lives in the infra/lucifer repository. 44 45 TODO(crbug.com/748234): Events starting with X are temporary to 46 support gradual lucifer rollout. 47 48 https://chromium.googlesource.com/chromiumos/infra/lucifer 49 """ 50 # Job status 51 STARTING = 'starting' 52 RUNNING = 'running' 53 GATHERING = 'gathering' 54 PARSING = 'parsing' 55 ABORTED = 'aborted' 56 COMPLETED = 'completed' 57 58 # Test status 59 TEST_PASSED = 'test_passed' 60 TEST_FAILED = 'test_failed' 61 62 # Host status 63 HOST_RUNNING = 'host_running' 64 HOST_READY = 'host_ready' 65 HOST_NEEDS_CLEANUP = 'host_needs_cleanup' 66 HOST_NEEDS_RESET = 'host_needs_reset' 67 68 # Temporary 69 X_TESTS_DONE = 'x_tests_done' # Only for GATHERING 70 71 72def run_event_command(event_handler, args): 73 """Run a command that emits events. 74 75 Events printed by the command to stdout will be handled by 76 event_handler synchronously. Exceptions raised by event_handler 77 will not be caught. If an exception escapes, the child process's 78 standard file descriptors are closed and the process is waited for. 79 The event command should terminate if this happens. 80 81 event_handler is called to handle each event. Malformed events 82 emitted by the command will be logged and discarded. The 83 event_handler should take two positional arguments: an Event 84 instance and a message string. 85 86 @param event_handler: event handler. 87 @param args: passed to subprocess.Popen. 88 @param returns: exit status of command. 89 """ 90 logger.debug('Starting event command with %r', args) 91 with subprocess32.Popen(args, stdout=PIPE, close_fds=True) as proc: 92 logger.debug('Event command child pid is %d', proc.pid) 93 _handle_subprocess_events(event_handler, proc) 94 logger.debug('Event command child with pid %d exited with %d', 95 proc.pid, proc.returncode) 96 return proc.returncode 97 98 99def _handle_subprocess_events(event_handler, proc): 100 """Handle a subprocess that emits events. 101 102 Events printed by the subprocess will be handled by event_handler. 103 104 @param event_handler: callable that takes an Event instance. 105 @param proc: Popen instance. 106 """ 107 while True: 108 logger.debug('Reading subprocess stdout') 109 line = proc.stdout.readline() 110 if not line: 111 break 112 _handle_output_line(event_handler, line) 113 114 115def _handle_output_line(event_handler, line): 116 """Handle a line of output from an event subprocess. 117 118 @param event_handler: callable that takes a StatusChangeEvent. 119 @param line: line of output. 120 """ 121 event_str, _, message = line.rstrip().partition(' ') 122 try: 123 event = Event(event_str) 124 except ValueError: 125 logger.warning('Invalid output %r received', line) 126 return 127 event_handler(event, message) 128