• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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