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 fcntl |
7 import logging | 8 import logging |
| 9 import os |
8 import pipes | 10 import pipes |
| 11 import select |
9 import signal | 12 import signal |
| 13 import StringIO |
10 import subprocess | 14 import subprocess |
11 import tempfile | 15 import time |
12 | |
13 from pylib.utils import timeout_retry | |
14 | 16 |
15 | 17 |
16 def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): | 18 def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): |
17 return subprocess.Popen( | 19 return subprocess.Popen( |
18 args=args, cwd=cwd, stdout=stdout, stderr=stderr, | 20 args=args, cwd=cwd, stdout=stdout, stderr=stderr, |
19 shell=shell, close_fds=True, env=env, | 21 shell=shell, close_fds=True, env=env, |
20 preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) | 22 preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) |
21 | 23 |
22 | 24 |
23 def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): | 25 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: | 83 elif shell: |
82 raise Exception('array args must be run with shell=False') | 84 raise Exception('array args must be run with shell=False') |
83 else: | 85 else: |
84 args_repr = ' '.join(map(pipes.quote, args)) | 86 args_repr = ' '.join(map(pipes.quote, args)) |
85 | 87 |
86 s = '[host]' | 88 s = '[host]' |
87 if cwd: | 89 if cwd: |
88 s += ':' + cwd | 90 s += ':' + cwd |
89 s += '> ' + args_repr | 91 s += '> ' + args_repr |
90 logging.info(s) | 92 logging.info(s) |
91 tmpout = tempfile.TemporaryFile(bufsize=0) | 93 pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
92 tmperr = tempfile.TemporaryFile(bufsize=0) | 94 shell=shell, cwd=cwd) |
93 exit_code = Call(args, cwd=cwd, stdout=tmpout, stderr=tmperr, shell=shell) | 95 stdout, stderr = pipe.communicate() |
94 tmperr.seek(0) | 96 |
95 stderr = tmperr.read() | |
96 tmperr.close() | |
97 if stderr: | 97 if stderr: |
98 logging.critical(stderr) | 98 logging.critical(stderr) |
99 tmpout.seek(0) | |
100 stdout = tmpout.read() | |
101 tmpout.close() | |
102 if len(stdout) > 4096: | 99 if len(stdout) > 4096: |
103 logging.debug('Truncated output:') | 100 logging.debug('Truncated output:') |
104 logging.debug(stdout[:4096]) | 101 logging.debug(stdout[:4096]) |
105 return (exit_code, stdout) | 102 return (pipe.returncode, stdout) |
106 | 103 |
107 | 104 |
108 def GetCmdStatusAndOutputWithTimeoutAndRetries(args, timeout, retries): | 105 class TimeoutError(Exception): |
109 """Executes a subprocess with a timeout and retries. | 106 """Module-specific timeout exception.""" |
| 107 pass |
| 108 |
| 109 |
| 110 def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False, |
| 111 logfile=None): |
| 112 """Executes a subprocess with a timeout. |
110 | 113 |
111 Args: | 114 Args: |
112 args: List of arguments to the program, the program to execute is the first | 115 args: List of arguments to the program, the program to execute is the first |
113 element. | 116 element. |
114 timeout: the timeout in seconds. | 117 timeout: the timeout in seconds or None to wait forever. |
115 retries: the number of retries. | 118 cwd: If not None, the subprocess's current directory will be changed to |
| 119 |cwd| before it's executed. |
| 120 shell: Whether to execute args as a shell command. |
| 121 logfile: Optional file-like object that will receive output from the |
| 122 command as it is running. |
116 | 123 |
117 Returns: | 124 Returns: |
118 The 2-tuple (exit code, output). | 125 The 2-tuple (exit code, output). |
119 """ | 126 """ |
120 return timeout_retry.Run(GetCmdStatusAndOutput, timeout, retries, [args]) | 127 try: |
| 128 process = Popen(args, cwd=cwd, stdout=subprocess.PIPE, shell=shell) |
| 129 end_time = (time.time() + timeout) if timeout else None |
| 130 poll_interval = 1 |
| 131 buffer_size = 4096 |
| 132 child_fd = process.stdout.fileno() |
| 133 output = StringIO.StringIO() |
| 134 |
| 135 # Enable non-blocking reads from the child's stdout. |
| 136 fl = fcntl.fcntl(child_fd, fcntl.F_GETFL) |
| 137 fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) |
| 138 |
| 139 while not process.poll(): |
| 140 if end_time and time.time() > end_time: |
| 141 raise TimeoutError |
| 142 read_fds, _, _ = select.select([child_fd], [], [], poll_interval) |
| 143 if child_fd in read_fds: |
| 144 data = os.read(child_fd, buffer_size) |
| 145 if not data: |
| 146 break |
| 147 if logfile: |
| 148 logfile.write(data) |
| 149 output.write(data) |
| 150 return process.returncode, output.getvalue() |
| 151 finally: |
| 152 if not process.poll(): |
| 153 process.kill() |
| 154 process.wait() |
OLD | NEW |