OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """ This module contains tools for running commands in a shell. """ |
| 7 |
| 8 import datetime |
| 9 import os |
| 10 import Queue |
| 11 import select |
| 12 import subprocess |
| 13 import sys |
| 14 import threading |
| 15 import time |
| 16 |
| 17 if 'nt' in os.name: |
| 18 import ctypes |
| 19 |
| 20 |
| 21 DEFAULT_SECS_BETWEEN_ATTEMPTS = 10 |
| 22 POLL_MILLIS = 250 |
| 23 |
| 24 |
| 25 class CommandFailedException(Exception): |
| 26 """Exception which gets raised when a command fails.""" |
| 27 |
| 28 def __init__(self, output, *args): |
| 29 """Initialize the CommandFailedException. |
| 30 |
| 31 Args: |
| 32 output: string; output from the command. |
| 33 """ |
| 34 Exception.__init__(self, *args) |
| 35 self._output = output |
| 36 |
| 37 @property |
| 38 def output(self): |
| 39 """Output from the command.""" |
| 40 return self._output |
| 41 |
| 42 |
| 43 class TimeoutException(CommandFailedException): |
| 44 """CommandFailedException which gets raised when a subprocess exceeds its |
| 45 timeout. """ |
| 46 pass |
| 47 |
| 48 |
| 49 def run_async(cmd, echo=True, shell=False): |
| 50 """ Run 'cmd' in a subprocess, returning a Popen class instance referring to |
| 51 that process. (Non-blocking) """ |
| 52 if echo: |
| 53 print cmd |
| 54 if 'nt' in os.name: |
| 55 # Windows has a bad habit of opening a dialog when a console program |
| 56 # crashes, rather than just letting it crash. Therefore, when a program |
| 57 # crashes on Windows, we don't find out until the build step times out. |
| 58 # This code prevents the dialog from appearing, so that we find out |
| 59 # immediately and don't waste time waiting around. |
| 60 SEM_NOGPFAULTERRORBOX = 0x0002 |
| 61 ctypes.windll.kernel32.SetErrorMode(SEM_NOGPFAULTERRORBOX) |
| 62 flags = 0x8000000 # CREATE_NO_WINDOW |
| 63 else: |
| 64 flags = 0 |
| 65 return subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT, |
| 66 stdout=subprocess.PIPE, creationflags=flags, |
| 67 bufsize=1) |
| 68 |
| 69 |
| 70 class EnqueueThread(threading.Thread): |
| 71 """ Reads and enqueues lines from a file. """ |
| 72 def __init__(self, file_obj, queue): |
| 73 threading.Thread.__init__(self) |
| 74 self._file = file_obj |
| 75 self._queue = queue |
| 76 self._stopped = False |
| 77 |
| 78 def run(self): |
| 79 if sys.platform.startswith('linux'): |
| 80 # Use a polling object to avoid the blocking call to readline(). |
| 81 poll = select.poll() |
| 82 poll.register(self._file, select.POLLIN) |
| 83 while not self._stopped: |
| 84 has_output = poll.poll(POLL_MILLIS) |
| 85 if has_output: |
| 86 line = self._file.readline() |
| 87 if line == '': |
| 88 self._stopped = True |
| 89 self._queue.put(line) |
| 90 else: |
| 91 # Only Unix supports polling objects, so just read from the file, |
| 92 # Python-style. |
| 93 for line in iter(self._file.readline, ''): |
| 94 self._queue.put(line) |
| 95 if self._stopped: |
| 96 break |
| 97 |
| 98 def stop(self): |
| 99 self._stopped = True |
| 100 |
| 101 |
| 102 def log_process_in_real_time(proc, echo=True, timeout=None, log_file=None, |
| 103 halt_on_output=None, print_timestamps=True): |
| 104 """ Log the output of proc in real time until it completes. Return a tuple |
| 105 containing the exit code of proc and the contents of stdout. |
| 106 |
| 107 proc: an instance of Popen referring to a running subprocess. |
| 108 echo: boolean indicating whether to print the output received from proc.stdout |
| 109 timeout: number of seconds allotted for the process to run. Raises a |
| 110 TimeoutException if the run time exceeds the timeout. |
| 111 log_file: an open file for writing output |
| 112 halt_on_output: string; kill the process and return if this string is found |
| 113 in the output stream from the process. |
| 114 print_timestamps: boolean indicating whether a formatted timestamp should be |
| 115 prepended to each line of output. |
| 116 """ |
| 117 stdout_queue = Queue.Queue() |
| 118 log_thread = EnqueueThread(proc.stdout, stdout_queue) |
| 119 log_thread.start() |
| 120 try: |
| 121 all_output = [] |
| 122 t_0 = time.time() |
| 123 while True: |
| 124 code = proc.poll() |
| 125 try: |
| 126 output = stdout_queue.get_nowait() |
| 127 all_output.append(output) |
| 128 if output and print_timestamps: |
| 129 timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f') |
| 130 output = ''.join(['[%s] %s\n' % (timestamp, line) |
| 131 for line in output.splitlines()]) |
| 132 if echo: |
| 133 sys.stdout.write(output) |
| 134 sys.stdout.flush() |
| 135 if log_file: |
| 136 log_file.write(output) |
| 137 log_file.flush() |
| 138 if halt_on_output and halt_on_output in output: |
| 139 proc.terminate() |
| 140 break |
| 141 except Queue.Empty: |
| 142 if code != None: # proc has finished running |
| 143 break |
| 144 time.sleep(0.5) |
| 145 if timeout and time.time() - t_0 > timeout: |
| 146 proc.terminate() |
| 147 raise TimeoutException( |
| 148 ''.join(all_output), |
| 149 'Subprocess exceeded timeout of %ds' % timeout) |
| 150 finally: |
| 151 log_thread.stop() |
| 152 log_thread.join() |
| 153 return (code, ''.join(all_output)) |
| 154 |
| 155 |
| 156 def log_process_after_completion(proc, echo=True, timeout=None, log_file=None): |
| 157 """ Wait for proc to complete and return a tuple containing the exit code of |
| 158 proc and the contents of stdout. Unlike log_process_in_real_time, does not |
| 159 attempt to read stdout from proc in real time. |
| 160 |
| 161 proc: an instance of Popen referring to a running subprocess. |
| 162 echo: boolean indicating whether to print the output received from proc.stdout |
| 163 timeout: number of seconds allotted for the process to run. Raises a |
| 164 TimeoutException if the run time exceeds the timeout. |
| 165 log_file: an open file for writing outout |
| 166 """ |
| 167 t_0 = time.time() |
| 168 code = None |
| 169 while code is None: |
| 170 if timeout and time.time() - t_0 > timeout: |
| 171 raise TimeoutException( |
| 172 proc.communicate()[0], |
| 173 'Subprocess exceeded timeout of %ds' % timeout) |
| 174 time.sleep(0.5) |
| 175 code = proc.poll() |
| 176 output = proc.communicate()[0] |
| 177 if echo: |
| 178 print output |
| 179 if log_file: |
| 180 log_file.write(output) |
| 181 log_file.flush() |
| 182 return (code, output) |
| 183 |
| 184 |
| 185 def run(cmd, echo=True, shell=False, timeout=None, print_timestamps=True, |
| 186 log_in_real_time=True): |
| 187 """ Run 'cmd' in a shell and return the combined contents of stdout and |
| 188 stderr (Blocking). Throws an exception if the command exits non-zero. |
| 189 |
| 190 cmd: list of strings (or single string, iff shell==True) indicating the |
| 191 command to run |
| 192 echo: boolean indicating whether we should print the command and log output |
| 193 shell: boolean indicating whether we are using advanced shell features. Use |
| 194 only when absolutely necessary, since this allows a lot more freedom which |
| 195 could be exploited by malicious code. See the warning here: |
| 196 http://docs.python.org/library/subprocess.html#popen-constructor |
| 197 timeout: optional, integer indicating the maximum elapsed time in seconds |
| 198 print_timestamps: boolean indicating whether a formatted timestamp should be |
| 199 prepended to each line of output. Unused if echo or log_in_real_time is |
| 200 False. |
| 201 log_in_real_time: boolean indicating whether to read stdout from the |
| 202 subprocess in real time instead of when the process finishes. If echo is |
| 203 False, we never log in real time, even if log_in_real_time is True. |
| 204 """ |
| 205 proc = run_async(cmd, echo=echo, shell=shell) |
| 206 # If we're not printing the output, we don't care if the output shows up in |
| 207 # real time, so don't bother. |
| 208 if log_in_real_time and echo: |
| 209 (returncode, output) = log_process_in_real_time(proc, echo=echo, |
| 210 timeout=timeout, print_timestamps=print_timestamps) |
| 211 else: |
| 212 (returncode, output) = log_process_after_completion(proc, echo=echo, |
| 213 timeout=timeout) |
| 214 if returncode != 0: |
| 215 raise CommandFailedException( |
| 216 output, |
| 217 'Command failed with code %d: %s' % (returncode, cmd)) |
| 218 return output |
| 219 |
| 220 |
| 221 def run_retry(cmd, echo=True, shell=False, attempts=1, |
| 222 secs_between_attempts=DEFAULT_SECS_BETWEEN_ATTEMPTS, |
| 223 timeout=None, print_timestamps=True): |
| 224 """ Wrapper for run() which makes multiple attempts until either the command |
| 225 succeeds or the maximum number of attempts is reached. """ |
| 226 attempt = 1 |
| 227 while True: |
| 228 try: |
| 229 return run(cmd, echo=echo, shell=shell, timeout=timeout, |
| 230 print_timestamps=print_timestamps) |
| 231 except CommandFailedException: |
| 232 if attempt >= attempts: |
| 233 raise |
| 234 print 'Command failed. Retrying in %d seconds...' % secs_between_attempts |
| 235 time.sleep(secs_between_attempts) |
| 236 attempt += 1 |
OLD | NEW |