Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(45)

Side by Side Diff: lib/cros_subprocess.py

Issue 6626039: Revert "Plumb in crprocess instead of RunCommand to allow quiet operation." (Closed) Base URL: http://git.chromium.org/git/chromite.git@master
Patch Set: Created 9 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « lib/cros_build_lib.py ('k') | lib/operation.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 2
3 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 3 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be 4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file. 5 # found in the LICENSE file.
6 # 6 #
7 # Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se> 7 # Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se>
8 # Licensed to PSF under a Contributor Agreement. 8 # Licensed to PSF under a Contributor Agreement.
9 # See http://www.python.org/2.4/license for licensing details. 9 # See http://www.python.org/2.4/license for licensing details.
10 10
11 """Subprocress execution 11 """Subprocress execution
12 12
13 This module holds a subclass of subprocess.Popen with our own required 13 This module holds a subclass of subprocess.Popen with our own required
14 features. 14 features.
15 """ 15 """
16 16
17 import errno 17 import errno
18 import os 18 import os
19 import pty 19 import pty
20 import select 20 import select
21 import subprocess 21 import subprocess
22 import sys 22 import sys
23 import unittest 23 import unittest
24 24
25 25
26 # Import these here so the caller does not need to import subprocess also.
27 PIPE = subprocess.PIPE
28 STDOUT = subprocess.STDOUT
29 PIPE_PTY = -3 # Pipe output through a pty
30
31 26
32 class Popen(subprocess.Popen): 27 class Popen(subprocess.Popen):
33 """Like subprocess.Popen with ptys and incremental output 28 """Like subprocess.Popen with ptys and incremental output
34 29
35 This class deals with running a child process and filtering its output on 30 This class deals with running a child process and filtering its output on
36 both stdout and stderr while it is running. We do this so we can monitor 31 both stdout and stderr while it is running. We do this so we can monitor
37 progress, and possibly relay the output to the user if requested. 32 progress, and possibly relay the output to the user if requested.
38 33
39 The class is similar to subprocess.Popen, the equivalent is something like: 34 The class is similar to subprocess.Popen, the equivalent is something like:
40 35
41 Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 36 Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
42 37
43 But this class has many fewer features, and two enhancement: 38 But this class has many fewer features, and two enhancement:
44 39
45 1. Rather than getting the output data only at the end, this class sends it 40 1. Rather than getting the output data only at the end, this class sends it
46 to a provided operation as it arrives. 41 to a provided operation as it arrives.
47 2. We use pseudo terminals so that the child will hopefully flush its output 42 2. We use pseudo terminals so that the child will hopefully flush its output
48 to us as soon as it is produced, rather than waiting for the end of a 43 to us as soon as it is produced, rather than waiting for the end of a
49 line. 44 line.
50 45
51 Use CommunicateFilter() to handle output from the subprocess. 46 Use CommunicateFilter() to handle output from the subprocess.
52 47
53 """ 48 """
49 PIPE_PTY = -3
54 50
55 def __init__(self, args, stdin=None, stdout=PIPE_PTY, stderr=PIPE_PTY, 51 def __init__(self, args, stdin=None, stdout=PIPE_PTY, stderr=PIPE_PTY,
56 shell=False, cwd=None, env=None, **kwargs): 52 shell=False, cwd=None, env=None, **kwargs):
57 """Cut-down constructor 53 """Cut-down constructor
58 54
59 Args: 55 Args:
60 args: Program and arguments for subprocess to execute. 56 args: Program and arguments for subprocess to execute.
61 stdin: See subprocess.Popen() 57 stdin: See subprocess.Popen()
62 stdout: See subprocess.Popen(), except that we support the sentinel 58 stdout: See subprocess.Popen(), except that we support the sentinel
63 value of cros_subprocess.PIPE_PTY. 59 value of cros_subprocess.Popen.PIPE_PTY.
64 stderr: See subprocess.Popen(), except that we support the sentinel 60 stderr: See subprocess.Popen(), except that we support the sentinel
65 value of cros_subprocess.PIPE_PTY. 61 value of cros_subprocess.Popen.PIPE_PTY.
66 shell: See subprocess.Popen() 62 shell: See subprocess.Popen()
67 cwd: Working directory to change to for subprocess, or None if none. 63 cwd: Working directory to change to for subprocess, or None if none.
68 env: Environment to use for this subprocess, or None to inherit parent. 64 env: Environment to use for this subprocess, or None to inherit parent.
69 kwargs: No other arguments are supported at the moment. Passing other 65 kwargs: No other arguments are supported at the moment. Passing other
70 arguments will cause a ValueError to be raised. 66 arguments will cause a ValueError to be raised.
71 """ 67 """
72 stdout_pty = None 68 stdout_pty = None
73 stderr_pty = None 69 stderr_pty = None
74 70
75 if stdout == PIPE_PTY: 71 if stdout == Popen.PIPE_PTY:
76 stdout_pty = pty.openpty() 72 stdout_pty = pty.openpty()
77 stdout = os.fdopen(stdout_pty[1]) 73 stdout = os.fdopen(stdout_pty[1])
78 if stderr == PIPE_PTY: 74 if stderr == Popen.PIPE_PTY:
79 stderr_pty = pty.openpty() 75 stderr_pty = pty.openpty()
80 stderr = os.fdopen(stderr_pty[1]) 76 stderr = os.fdopen(stderr_pty[1])
81 77
82 super(Popen, self).__init__(args, stdin=stdin, 78 super(Popen, self).__init__(args, stdin=stdin,
83 stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, env=env, 79 stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, env=env,
84 **kwargs) 80 **kwargs)
85 81
86 # If we're on a PTY, we passed the slave half of the PTY to the subprocess. 82 # If we're on a PTY, we passed the slave half of the PTY to the subprocess.
87 # We want to use the master half on our end from now on. Setting this here 83 # We want to use the master half on our end from now on. Setting this here
88 # does make some assumptions about the implementation of subprocess, but 84 # does make some assumptions about the implementation of subprocess, but
89 # those assumptions are pretty minor. 85 # those assumptions are pretty minor.
90
91 # Note that if stderr is STDOUT, then self.stderr will be set to None by
92 # this constructor.
93 if stdout_pty is not None: 86 if stdout_pty is not None:
94 self.stdout = os.fdopen(stdout_pty[0]) 87 self.stdout = os.fdopen(stdout_pty[0])
95 if stderr_pty is not None: 88 if stderr_pty is not None:
96 self.stderr = os.fdopen(stderr_pty[0]) 89 self.stderr = os.fdopen(stderr_pty[0])
97 90
98 # Insist that unit tests exist for other arguments we don't support. 91 # Insist that unit tests exist for other arguments we don't support.
99 if kwargs: 92 if kwargs:
100 raise ValueError("Unit tests do not test extra args - please add tests") 93 raise ValueError("Unit tests do not test extra args - please add tests")
101 94
102 def CommunicateFilter(self, output): 95 def CommunicateFilter(self, operation):
103 """Interact with process: Read data from stdout and stderr. 96 """Interact with process: Read data from stdout and stderr.
104 97
105 This method runs until end-of-file is reached, then waits for the 98 This method runs until end-of-file is reached, then waits for the
106 subprocess to terminate. 99 subprocess to terminate.
107 100
108 The output function is sent all output from the subprocess and must be 101 The operation object is send all output from the subprocess through its
109 defined like this: 102 Output() method which must be defined like this:
110 103
111 def Output([self,] stream, data) 104 def Output(self, stream, data)
112 Args: 105 Args:
113 stream: the stream the output was received on, which will be 106 stream: the stream the output was received on, which will be
114 sys.stdout or sys.stderr. 107 sys.stdout or sys.stderr.
115 data: a string containing the data 108 data: a string containing the data
116 109
117 Note: The data read is buffered in memory, so do not use this 110 Note: The data read is buffered in memory, so do not use this
118 method if the data size is large or unlimited. 111 method if the data size is large or unlimited.
119 112
120 Args: 113 Args:
121 output: Function to call with each fragment of output. 114 operation: Operation to use for this subprocess.
122 115
123 Returns: 116 Returns:
124 A tuple (stdout, stderr, combined) which is the data received on 117 A tuple (stdout, stderr, combined) which is the data received on
125 stdout, stderr and the combined data (interleaved stdout and stderr). 118 stdout, stderr and the combined data (interleaved stdout and stderr).
126
127 Note that the interleaved output will only be sensible if you have
128 set both stdout and stderr to PIPE or PIPE_PTY. Even then it depends on
129 the timing of the output in the subprocess. If a subprocess flips
130 between stdout and stderr quickly in succession, by the time we come to
131 read the output from each we may see several lines in each, and will read
132 all the stdout lines, then all the stderr lines. So the interleaving
133 may not be correct. In this case you might want to pass
134 stderr=cros_subprocess.STDOUT to the constructor.
135
136 This feature is still useful for subprocesses where stderr is
137 rarely used and indicates an error.
138
139 Note also that if you set stderr to STDOUT, then stderr will be empty
140 and the combined output will just be the same as stdout.
141 """ 119 """
142 120
143 read_set = [] 121 read_set = []
144 write_set = [] 122 write_set = []
145 stdout = None # Return 123 stdout = None # Return
146 stderr = None # Return 124 stderr = None # Return
147 125
148 if self.stdin: 126 if self.stdin:
149 # Flush stdio buffer. This might block, if the user has 127 # Flush stdio buffer. This might block, if the user has
150 # been writing to .stdin in an uncontrolled fashion. 128 # been writing to .stdin in an uncontrolled fashion.
151 self.stdin.flush() 129 self.stdin.flush()
152 if input: 130 if input:
153 write_set.append(self.stdin) 131 write_set.append(self.stdin)
154 else: 132 else:
155 self.stdin.close() 133 self.stdin.close()
156 if self.stdout: 134 if self.stdout:
157 read_set.append(self.stdout) 135 read_set.append(self.stdout)
158 stdout = [] 136 stdout = []
159 if self.stderr and self.stderr != self.stdout: 137 if self.stderr:
160 read_set.append(self.stderr) 138 read_set.append(self.stderr)
161 stderr = [] 139 stderr = []
162 combined = [] 140 combined = []
163 141
164 input_offset = 0 142 input_offset = 0
165 while read_set or write_set: 143 while read_set or write_set:
166 try: 144 try:
167 rlist, wlist, _ = select.select(read_set, write_set, []) 145 rlist, wlist, _ = select.select(read_set, write_set, [])
168 except select.error, e: 146 except select.error, e:
169 if e.args[0] == errno.EINTR: 147 if e.args[0] == errno.EINTR:
(...skipping 17 matching lines...) Expand all
187 try: 165 try:
188 data = os.read(self.stdout.fileno(), 1024) 166 data = os.read(self.stdout.fileno(), 1024)
189 except OSError: 167 except OSError:
190 pass 168 pass
191 if data == "": 169 if data == "":
192 self.stdout.close() 170 self.stdout.close()
193 read_set.remove(self.stdout) 171 read_set.remove(self.stdout)
194 else: 172 else:
195 stdout.append(data) 173 stdout.append(data)
196 combined.append(data) 174 combined.append(data)
197 output(sys.stdout, data) 175 operation.Output(sys.stdout, data)
198 if self.stderr in rlist: 176 if self.stderr in rlist:
199 data = "" 177 data = ""
200 # We will get an error on read if the pty is closed 178 # We will get an error on read if the pty is closed
201 try: 179 try:
202 data = os.read(self.stderr.fileno(), 1024) 180 data = os.read(self.stderr.fileno(), 1024)
203 except OSError: 181 except OSError:
204 pass 182 pass
205 if data == "": 183 if data == "":
206 self.stderr.close() 184 self.stderr.close()
207 read_set.remove(self.stderr) 185 read_set.remove(self.stderr)
208 else: 186 else:
209 stderr.append(data) 187 stderr.append(data)
210 combined.append(data) 188 combined.append(data)
211 output(sys.stderr, data) 189 operation.Output(sys.stderr, data)
212 190
213 # All data exchanged. Translate lists into strings. 191 # All data exchanged. Translate lists into strings.
214 if stdout is not None: 192 if stdout is not None:
215 stdout = ''.join(stdout) 193 stdout = ''.join(stdout)
216 if stderr is not None: 194 if stderr is not None:
217 stderr = ''.join(stderr) 195 stderr = ''.join(stderr)
218 combined = ''.join(combined) 196 combined = ''.join(combined)
219 197
220 # Translate newlines, if requested. We cannot let the file 198 # Translate newlines, if requested. We cannot let the file
221 # object do the translation: It is based on stdio, which is 199 # object do the translation: It is based on stdio, which is
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after
276 self.assertEqual(plist[0], oper.stdout_data) 254 self.assertEqual(plist[0], oper.stdout_data)
277 self.assertEqual(plist[1], oper.stderr_data) 255 self.assertEqual(plist[1], oper.stderr_data)
278 self.assertEqual(plist[2], oper.combined_data) 256 self.assertEqual(plist[2], oper.combined_data)
279 257
280 # The total length of stdout and stderr should equal the combined length 258 # The total length of stdout and stderr should equal the combined length
281 self.assertEqual(len(plist[0]) + len(plist[1]), len(plist[2])) 259 self.assertEqual(len(plist[0]) + len(plist[1]), len(plist[2]))
282 260
283 def test_simple(self): 261 def test_simple(self):
284 """Simple redirection: Get process list""" 262 """Simple redirection: Get process list"""
285 oper = TestSubprocess.MyOperation() 263 oper = TestSubprocess.MyOperation()
286 plist = Popen(['ps']).CommunicateFilter(oper.Output) 264 plist = Popen(['ps']).CommunicateFilter(oper)
287 self._BasicCheck(plist, oper) 265 self._BasicCheck(plist, oper)
288 266
289 def test_stderr(self): 267 def test_stderr(self):
290 """Check stdout and stderr""" 268 """Check stdout and stderr"""
291 oper = TestSubprocess.MyOperation() 269 oper = TestSubprocess.MyOperation()
292 cmd = 'echo fred >/dev/stderr && false || echo bad' 270 cmd = 'echo fred >/dev/stderr && false || echo bad'
293 plist = Popen([cmd], shell=True).CommunicateFilter(oper.Output) 271 plist = Popen([cmd], shell=True).CommunicateFilter(oper)
294 self._BasicCheck(plist, oper) 272 self._BasicCheck(plist, oper)
295 self.assertEqual(plist [0], 'bad\r\n') 273 self.assertEqual(plist [0], 'bad\r\n')
296 self.assertEqual(plist [1], 'fred\r\n') 274 self.assertEqual(plist [1], 'fred\r\n')
297 275
298 def test_shell(self): 276 def test_shell(self):
299 """Check with and without shell works""" 277 """Check with and without shell works"""
300 oper = TestSubprocess.MyOperation() 278 oper = TestSubprocess.MyOperation()
301 cmd = 'echo test >/dev/stderr' 279 cmd = 'echo test >/dev/stderr'
302 self.assertRaises(OSError, Popen, [cmd], shell=False) 280 self.assertRaises(OSError, Popen, [cmd], shell=False)
303 plist = Popen([cmd], shell=True).CommunicateFilter(oper.Output) 281 plist = Popen([cmd], shell=True).CommunicateFilter(oper)
304 self._BasicCheck(plist, oper) 282 self._BasicCheck(plist, oper)
305 self.assertEqual(len(plist [0]), 0) 283 self.assertEqual(len(plist [0]), 0)
306 self.assertEqual(plist [1], 'test\r\n') 284 self.assertEqual(plist [1], 'test\r\n')
307 285
308 def test_list_args(self): 286 def test_list_args(self):
309 """Check with and without shell works using list arguments""" 287 """Check with and without shell works using list arguments"""
310 oper = TestSubprocess.MyOperation() 288 oper = TestSubprocess.MyOperation()
311 cmd = ['echo', 'test', '>/dev/stderr'] 289 cmd = ['echo', 'test', '>/dev/stderr']
312 plist = Popen(cmd, shell=False).CommunicateFilter(oper.Output) 290 plist = Popen(cmd, shell=False).CommunicateFilter(oper)
313 self._BasicCheck(plist, oper) 291 self._BasicCheck(plist, oper)
314 self.assertEqual(plist [0], ' '.join(cmd[1:]) + '\r\n') 292 self.assertEqual(plist [0], ' '.join(cmd[1:]) + '\r\n')
315 self.assertEqual(len(plist [1]), 0) 293 self.assertEqual(len(plist [1]), 0)
316 294
317 oper = TestSubprocess.MyOperation() 295 oper = TestSubprocess.MyOperation()
318 296
319 # this should be interpreted as 'echo' with the other args dropped 297 # this should be interpreted as 'echo' with the other args dropped
320 cmd = ['echo', 'test', '>/dev/stderr'] 298 cmd = ['echo', 'test', '>/dev/stderr']
321 plist = Popen(cmd, shell=True).CommunicateFilter(oper.Output) 299 plist = Popen(cmd, shell=True).CommunicateFilter(oper)
322 self._BasicCheck(plist, oper) 300 self._BasicCheck(plist, oper)
323 self.assertEqual(plist [0], '\r\n') 301 self.assertEqual(plist [0], '\r\n')
324 302
325 def test_cwd(self): 303 def test_cwd(self):
326 """Check we can change directory""" 304 """Check we can change directory"""
327 for shell in (False, True): 305 for shell in (False, True):
328 oper = TestSubprocess.MyOperation() 306 oper = TestSubprocess.MyOperation()
329 plist = Popen('pwd', shell=shell, cwd='/tmp').CommunicateFilter(oper.Outpu t) 307 plist = Popen('pwd', shell=shell, cwd='/tmp').CommunicateFilter(oper)
330 self._BasicCheck(plist, oper) 308 self._BasicCheck(plist, oper)
331 self.assertEqual(plist [0], '/tmp\r\n') 309 self.assertEqual(plist [0], '/tmp\r\n')
332 310
333 def test_env(self): 311 def test_env(self):
334 """Check we can change environment""" 312 """Check we can change environment"""
335 for add in (False, True): 313 for add in (False, True):
336 oper = TestSubprocess.MyOperation() 314 oper = TestSubprocess.MyOperation()
337 env = os.environ 315 env = os.environ
338 if add: 316 if add:
339 env ['FRED'] = 'fred' 317 env ['FRED'] = 'fred'
340 cmd = 'echo $FRED' 318 cmd = 'echo $FRED'
341 plist = Popen(cmd, shell=True, env=env).CommunicateFilter(oper.Output) 319 plist = Popen(cmd, shell=True, env=env).CommunicateFilter(oper)
342 self._BasicCheck(plist, oper) 320 self._BasicCheck(plist, oper)
343 self.assertEqual(plist [0], add and 'fred\r\n' or '\r\n') 321 self.assertEqual(plist [0], add and 'fred\r\n' or '\r\n')
344 322
345 def test_extra_args(self): 323 def test_extra_args(self):
346 """Check we can't add extra arguments""" 324 """Check we can't add extra arguments"""
347 self.assertRaises(ValueError, Popen, 'true', close_fds=False) 325 self.assertRaises(ValueError, Popen, 'true', close_fds=False)
348 326
349 def test_basic_input(self): 327 def test_basic_input(self):
350 """Check that incremental input works 328 """Check that incremental input works
351 329
352 We set up a subprocess which will prompt for name. When we see this prompt 330 We set up a subprocess which will prompt for name. When we see this prompt
353 we send the name as input to the process. It should then print the name 331 we send the name as input to the process. It should then print the name
354 properly to stdout. 332 properly to stdout.
355 """ 333 """
356 oper = TestSubprocess.MyOperation('Flash') 334 oper = TestSubprocess.MyOperation('Flash')
357 prompt = 'What is your name?: ' 335 prompt = 'What is your name?: '
358 cmd = 'echo -n "%s"; read name; echo Hello $name' % prompt 336 cmd = 'echo -n "%s"; read name; echo Hello $name' % prompt
359 plist = Popen([cmd], stdin=oper.stdin_read_pipe, 337 plist = Popen([cmd], stdin=oper.stdin_read_pipe,
360 shell=True).CommunicateFilter(oper.Output) 338 shell=True).CommunicateFilter(oper)
361 self._BasicCheck(plist, oper) 339 self._BasicCheck(plist, oper)
362 self.assertEqual(len(plist [1]), 0) 340 self.assertEqual(len(plist [1]), 0)
363 self.assertEqual(plist [0], prompt + 'Hello Flash\r\r\n') 341 self.assertEqual(plist [0], prompt + 'Hello Flash\r\r\n')
364 342
365 #TODO(sjg): Add test for passing PIPE in case underlying subprocess breaks. 343 #TODO(sjg): Add test for passing PIPE in case underlying subprocess breaks.
366 #TODO(sjg): Add test for passing a file handle also. 344 #TODO(sjg): Add test for passing a file handle also.
367 345
368 def test_isatty(self): 346 def test_isatty(self):
369 """Check that ptys appear as terminals to the subprocess""" 347 """Check that ptys appear as terminals to the subprocess"""
370 oper = TestSubprocess.MyOperation() 348 oper = TestSubprocess.MyOperation()
371 cmd = ('if [ -t %d ]; then echo "terminal %d" >&%d; ' 349 cmd = ('if [ -t %d ]; then echo "terminal %d" >&%d; '
372 'else echo "not %d" >&%d; fi;') 350 'else echo "not %d" >&%d; fi;')
373 both_cmds = '' 351 both_cmds = ''
374 for fd in (1, 2): 352 for fd in (1, 2):
375 both_cmds += cmd % (fd, fd, fd, fd, fd) 353 both_cmds += cmd % (fd, fd, fd, fd, fd)
376 plist = Popen(both_cmds, shell=True).CommunicateFilter(oper.Output) 354 plist = Popen(both_cmds, shell=True).CommunicateFilter(oper)
377 self._BasicCheck(plist, oper) 355 self._BasicCheck(plist, oper)
378 self.assertEqual(plist [0], 'terminal 1\r\n') 356 self.assertEqual(plist [0], 'terminal 1\r\n')
379 self.assertEqual(plist [1], 'terminal 2\r\n') 357 self.assertEqual(plist [1], 'terminal 2\r\n')
380 358
381 # Now try with PIPE and make sure it is not a terminal 359 # Now try with PIPE and make sure it is not a terminal
382 oper = TestSubprocess.MyOperation() 360 oper = TestSubprocess.MyOperation()
383 plist = Popen(both_cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 361 plist = Popen(both_cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
384 shell=True).CommunicateFilter(oper.Output) 362 shell=True).CommunicateFilter(oper)
385 self._BasicCheck(plist, oper) 363 self._BasicCheck(plist, oper)
386 self.assertEqual(plist [0], 'not 1\n') 364 self.assertEqual(plist [0], 'not 1\n')
387 self.assertEqual(plist [1], 'not 2\n') 365 self.assertEqual(plist [1], 'not 2\n')
388 366
389 if __name__ == '__main__': 367 if __name__ == '__main__':
390 unittest.main() 368 unittest.main()
OLDNEW
« no previous file with comments | « lib/cros_build_lib.py ('k') | lib/operation.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698