| #!/usr/bin/env python |
| # Copyright (c) 2012 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. |
| |
| """ This module contains tools for running commands in a shell. """ |
| |
| import datetime |
| import os |
| import queue |
| import select |
| import subprocess |
| import sys |
| import threading |
| import time |
| |
| if 'nt' in os.name: |
| import ctypes |
| |
| |
| DEFAULT_SECS_BETWEEN_ATTEMPTS = 10 |
| POLL_MILLIS = 250 |
| VERBOSE = True |
| |
| |
| class CommandFailedException(Exception): |
| """Exception which gets raised when a command fails.""" |
| |
| def __init__(self, output, *args): |
| """Initialize the CommandFailedException. |
| |
| Args: |
| output: string; output from the command. |
| """ |
| Exception.__init__(self, *args) |
| self._output = output |
| |
| @property |
| def output(self): |
| """Output from the command.""" |
| return self._output |
| |
| |
| class TimeoutException(CommandFailedException): |
| """CommandFailedException which gets raised when a subprocess exceeds its |
| timeout. """ |
| pass |
| |
| |
| def run_async(cmd, echo=None, shell=False): |
| """ Run 'cmd' in a subprocess, returning a Popen class instance referring to |
| that process. (Non-blocking) """ |
| if echo is None: |
| echo = VERBOSE |
| if echo: |
| print(' '.join(cmd) if isinstance(cmd, list) else cmd) |
| if 'nt' in os.name: |
| # Windows has a bad habit of opening a dialog when a console program |
| # crashes, rather than just letting it crash. Therefore, when a program |
| # crashes on Windows, we don't find out until the build step times out. |
| # This code prevents the dialog from appearing, so that we find out |
| # immediately and don't waste time waiting around. |
| SEM_NOGPFAULTERRORBOX = 0x0002 |
| ctypes.windll.kernel32.SetErrorMode(SEM_NOGPFAULTERRORBOX) |
| flags = 0x8000000 # CREATE_NO_WINDOW |
| else: |
| flags = 0 |
| return subprocess.Popen(cmd, shell=shell, stderr=subprocess.STDOUT, |
| stdout=subprocess.PIPE, creationflags=flags) |
| |
| |
| class EnqueueThread(threading.Thread): |
| """ Reads and enqueues lines from a file. """ |
| def __init__(self, file_obj, q): |
| threading.Thread.__init__(self) |
| self._file = file_obj |
| self._queue = q |
| self._stopped = False |
| |
| def run(self): |
| if sys.platform.startswith('linux'): |
| # Use a polling object to avoid the blocking call to readline(). |
| poll = select.poll() |
| poll.register(self._file, select.POLLIN) |
| while not self._stopped: |
| has_output = poll.poll(POLL_MILLIS) |
| if has_output: |
| line = self._file.readline() |
| if line == '': |
| self._stopped = True |
| self._queue.put(line) |
| else: |
| # Only Unix supports polling objects, so just read from the file, |
| # Python-style. |
| for line in iter(self._file.readline, ''): |
| self._queue.put(line) |
| if self._stopped: |
| break |
| |
| def stop(self): |
| self._stopped = True |
| |
| |
| def log_process_in_real_time(proc, echo=None, timeout=None, log_file=None, |
| halt_on_output=None, print_timestamps=True): |
| """ Log the output of proc in real time until it completes. Return a tuple |
| containing the exit code of proc and the contents of stdout. |
| |
| proc: an instance of Popen referring to a running subprocess. |
| echo: boolean indicating whether to print the output received from proc.stdout |
| timeout: number of seconds allotted for the process to run. Raises a |
| TimeoutException if the run time exceeds the timeout. |
| log_file: an open file for writing output |
| halt_on_output: string; kill the process and return if this string is found |
| in the output stream from the process. |
| print_timestamps: boolean indicating whether a formatted timestamp should be |
| prepended to each line of output. |
| """ |
| if echo is None: |
| echo = VERBOSE |
| stdout_queue = queue.Queue() |
| log_thread = EnqueueThread(proc.stdout, stdout_queue) |
| log_thread.start() |
| try: |
| all_output = [] |
| t_0 = time.time() |
| while True: |
| code = proc.poll() |
| try: |
| output = stdout_queue.get_nowait().decode('utf-8') |
| all_output.append(output) |
| if output and print_timestamps: |
| timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f') |
| output = ''.join(['[%s] %s\n' % (timestamp, line) |
| for line in output.splitlines()]) |
| if echo: |
| sys.stdout.write(output) |
| sys.stdout.flush() |
| if log_file: |
| log_file.write(output) |
| log_file.flush() |
| if halt_on_output and halt_on_output in output: |
| proc.terminate() |
| break |
| except queue.Empty: |
| if code != None: # proc has finished running |
| break |
| time.sleep(0.5) |
| if timeout and time.time() - t_0 > timeout: |
| proc.terminate() |
| raise TimeoutException( |
| ''.join(all_output), |
| 'Subprocess exceeded timeout of %ds' % timeout) |
| finally: |
| log_thread.stop() |
| log_thread.join() |
| return (code, ''.join(all_output)) |
| |
| |
| def log_process_after_completion(proc, echo=None, timeout=None, |
| log_file=None): |
| """ Wait for proc to complete and return a tuple containing the exit code of |
| proc and the contents of stdout. Unlike log_process_in_real_time, does not |
| attempt to read stdout from proc in real time. |
| |
| proc: an instance of Popen referring to a running subprocess. |
| echo: boolean indicating whether to print the output received from proc.stdout |
| timeout: number of seconds allotted for the process to run. Raises a |
| TimeoutException if the run time exceeds the timeout. |
| log_file: an open file for writing outout |
| """ |
| if echo is None: |
| echo = VERBOSE |
| t_0 = time.time() |
| code = None |
| while code is None: |
| if timeout and time.time() - t_0 > timeout: |
| raise TimeoutException( |
| proc.communicate()[0], |
| 'Subprocess exceeded timeout of %ds' % timeout) |
| time.sleep(0.5) |
| code = proc.poll() |
| output = proc.communicate()[0] |
| if echo: |
| print(output) |
| if log_file: |
| log_file.write(output) |
| log_file.flush() |
| return (code, output) |
| |
| |
| def run(cmd, echo=None, shell=False, timeout=None, print_timestamps=True, |
| log_in_real_time=True): |
| """ Run 'cmd' in a shell and return the combined contents of stdout and |
| stderr (Blocking). Throws an exception if the command exits non-zero. |
| |
| cmd: list of strings (or single string, iff shell==True) indicating the |
| command to run |
| echo: boolean indicating whether we should print the command and log output |
| shell: boolean indicating whether we are using advanced shell features. Use |
| only when absolutely necessary, since this allows a lot more freedom which |
| could be exploited by malicious code. See the warning here: |
| http://docs.python.org/library/subprocess.html#popen-constructor |
| timeout: optional, integer indicating the maximum elapsed time in seconds |
| print_timestamps: boolean indicating whether a formatted timestamp should be |
| prepended to each line of output. Unused if echo or log_in_real_time is |
| False. |
| log_in_real_time: boolean indicating whether to read stdout from the |
| subprocess in real time instead of when the process finishes. If echo is |
| False, we never log in real time, even if log_in_real_time is True. |
| """ |
| if echo is None: |
| echo = VERBOSE |
| proc = run_async(cmd, echo=echo, shell=shell) |
| # If we're not printing the output, we don't care if the output shows up in |
| # real time, so don't bother. |
| if log_in_real_time and echo: |
| (returncode, output) = log_process_in_real_time(proc, echo=echo, |
| timeout=timeout, print_timestamps=print_timestamps) |
| else: |
| (returncode, output) = log_process_after_completion(proc, echo=echo, |
| timeout=timeout) |
| if returncode != 0: |
| raise CommandFailedException( |
| output, |
| 'Command failed with code %d: %s' % (returncode, cmd)) |
| return output |
| |
| |
| def run_retry(cmd, echo=None, shell=False, attempts=1, |
| secs_between_attempts=DEFAULT_SECS_BETWEEN_ATTEMPTS, |
| timeout=None, print_timestamps=True): |
| """ Wrapper for run() which makes multiple attempts until either the command |
| succeeds or the maximum number of attempts is reached. """ |
| if echo is None: |
| echo = VERBOSE |
| attempt = 1 |
| while True: |
| try: |
| return run(cmd, echo=echo, shell=shell, timeout=timeout, |
| print_timestamps=print_timestamps) |
| except CommandFailedException: |
| if attempt >= attempts: |
| raise |
| print('Command failed. Retrying in %d seconds...' % secs_between_attempts) |
| time.sleep(secs_between_attempts) |
| attempt += 1 |