OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """A wrapper for subprocess to make calling shell commands easier.""" | 5 """A wrapper for subprocess to make calling shell commands easier.""" |
6 | 6 |
7 import logging | 7 import logging |
| 8 import os |
8 import pipes | 9 import pipes |
| 10 import select |
9 import signal | 11 import signal |
| 12 import StringIO |
10 import subprocess | 13 import subprocess |
11 import tempfile | 14 import time |
12 | 15 |
13 from pylib.utils import timeout_retry | 16 # fcntl is not available on Windows. |
| 17 try: |
| 18 import fcntl |
| 19 except ImportError: |
| 20 fcntl = None |
14 | 21 |
15 | 22 |
16 def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): | 23 def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): |
17 return subprocess.Popen( | 24 return subprocess.Popen( |
18 args=args, cwd=cwd, stdout=stdout, stderr=stderr, | 25 args=args, cwd=cwd, stdout=stdout, stderr=stderr, |
19 shell=shell, close_fds=True, env=env, | 26 shell=shell, close_fds=True, env=env, |
20 preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) | 27 preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) |
21 | 28 |
22 | 29 |
23 def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): | 30 def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
81 elif shell: | 88 elif shell: |
82 raise Exception('array args must be run with shell=False') | 89 raise Exception('array args must be run with shell=False') |
83 else: | 90 else: |
84 args_repr = ' '.join(map(pipes.quote, args)) | 91 args_repr = ' '.join(map(pipes.quote, args)) |
85 | 92 |
86 s = '[host]' | 93 s = '[host]' |
87 if cwd: | 94 if cwd: |
88 s += ':' + cwd | 95 s += ':' + cwd |
89 s += '> ' + args_repr | 96 s += '> ' + args_repr |
90 logging.info(s) | 97 logging.info(s) |
91 tmpout = tempfile.TemporaryFile(bufsize=0) | 98 pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
92 tmperr = tempfile.TemporaryFile(bufsize=0) | 99 shell=shell, cwd=cwd) |
93 exit_code = Call(args, cwd=cwd, stdout=tmpout, stderr=tmperr, shell=shell) | 100 stdout, stderr = pipe.communicate() |
94 tmperr.seek(0) | 101 |
95 stderr = tmperr.read() | |
96 tmperr.close() | |
97 if stderr: | 102 if stderr: |
98 logging.critical(stderr) | 103 logging.critical(stderr) |
99 tmpout.seek(0) | |
100 stdout = tmpout.read() | |
101 tmpout.close() | |
102 if len(stdout) > 4096: | 104 if len(stdout) > 4096: |
103 logging.debug('Truncated output:') | 105 logging.debug('Truncated output:') |
104 logging.debug(stdout[:4096]) | 106 logging.debug(stdout[:4096]) |
105 return (exit_code, stdout) | 107 return (pipe.returncode, stdout) |
106 | 108 |
107 | 109 |
108 def GetCmdStatusAndOutputWithTimeoutAndRetries(args, timeout, retries): | 110 class TimeoutError(Exception): |
109 """Executes a subprocess with a timeout and retries. | 111 """Module-specific timeout exception.""" |
| 112 pass |
| 113 |
| 114 |
| 115 def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False, |
| 116 logfile=None): |
| 117 """Executes a subprocess with a timeout. |
110 | 118 |
111 Args: | 119 Args: |
112 args: List of arguments to the program, the program to execute is the first | 120 args: List of arguments to the program, the program to execute is the first |
113 element. | 121 element. |
114 timeout: the timeout in seconds. | 122 timeout: the timeout in seconds or None to wait forever. |
115 retries: the number of retries. | 123 cwd: If not None, the subprocess's current directory will be changed to |
| 124 |cwd| before it's executed. |
| 125 shell: Whether to execute args as a shell command. |
| 126 logfile: Optional file-like object that will receive output from the |
| 127 command as it is running. |
116 | 128 |
117 Returns: | 129 Returns: |
118 The 2-tuple (exit code, output). | 130 The 2-tuple (exit code, output). |
119 """ | 131 """ |
120 return timeout_retry.Run(GetCmdStatusAndOutput, timeout, retries, [args]) | 132 assert fcntl, 'fcntl module is required' |
| 133 process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE, |
| 134 stderr=subprocess.STDOUT) |
| 135 try: |
| 136 end_time = (time.time() + timeout) if timeout else None |
| 137 poll_interval = 1 |
| 138 buffer_size = 4096 |
| 139 child_fd = process.stdout.fileno() |
| 140 output = StringIO.StringIO() |
| 141 |
| 142 # Enable non-blocking reads from the child's stdout. |
| 143 fl = fcntl.fcntl(child_fd, fcntl.F_GETFL) |
| 144 fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) |
| 145 |
| 146 while True: |
| 147 if end_time and time.time() > end_time: |
| 148 raise TimeoutError |
| 149 read_fds, _, _ = select.select([child_fd], [], [], poll_interval) |
| 150 if child_fd in read_fds: |
| 151 data = os.read(child_fd, buffer_size) |
| 152 if not data: |
| 153 break |
| 154 if logfile: |
| 155 logfile.write(data) |
| 156 output.write(data) |
| 157 if process.poll() is not None: |
| 158 break |
| 159 finally: |
| 160 try: |
| 161 # Make sure the process doesn't stick around if we fail with an |
| 162 # exception. |
| 163 process.kill() |
| 164 except OSError: |
| 165 pass |
| 166 process.wait() |
| 167 return process.returncode, output.getvalue() |
OLD | NEW |