blob: c6ff24f6b2505483a449e9dbbb65515a68db1f3f [file] [log] [blame]
maruel@chromium.org4860f052011-03-25 20:34:38 +00001# coding=utf8
2# Copyright (c) 2011 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"""Collection of subprocess wrapper functions.
6
7In theory you shouldn't need anything else in subprocess, or this module failed.
8"""
9
maruel@chromium.org45d8db02011-03-31 20:43:56 +000010from __future__ import with_statement
maruel@chromium.org4860f052011-03-25 20:34:38 +000011import logging
12import os
13import subprocess
14import sys
15import tempfile
16import time
17import threading
18
19# Constants forwarded from subprocess.
20PIPE = subprocess.PIPE
21STDOUT = subprocess.STDOUT
22
23# Globals.
24# Set to True if you somehow need to disable this hack.
25SUBPROCESS_CLEANUP_HACKED = False
26
27
28class CalledProcessError(subprocess.CalledProcessError):
29 """Augment the standard exception with more data."""
30 def __init__(self, returncode, cmd, cwd, stdout, stderr):
31 super(CalledProcessError, self).__init__(returncode, cmd)
32 self.stdout = stdout
33 self.stderr = stderr
34 self.cwd = cwd
35
36 def __str__(self):
37 out = 'Command %s returned non-zero exit status %s' % (
38 ' '.join(self.cmd), self.returncode)
39 if self.cwd:
40 out += ' in ' + self.cwd
41 return '\n'.join(filter(None, (out, self.stdout, self.stderr)))
42
43
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +000044## Utility functions
45
46
47def kill_pid(pid):
48 """Kills a process by its process id."""
49 try:
50 # Unable to import 'module'
51 # pylint: disable=F0401
52 import signal
53 return os.kill(pid, signal.SIGKILL)
54 except ImportError:
55 pass
56
57
58def kill_win(process):
59 """Kills a process with its windows handle.
60
61 Has no effect on other platforms.
62 """
63 try:
64 # Unable to import 'module'
65 # pylint: disable=F0401
66 import win32process
67 # Access to a protected member _handle of a client class
68 # pylint: disable=W0212
69 return win32process.TerminateProcess(process._handle, -1)
70 except ImportError:
71 pass
72
73
74def add_kill():
75 """Adds kill() method to subprocess.Popen for python <2.6"""
76 if hasattr(subprocess.Popen, 'kill'):
77 return
78
79 if sys.platform == 'win32':
80 subprocess.Popen.kill = kill_win
81 else:
82 subprocess.Popen.kill = lambda process: kill_pid(process.pid)
83
84
maruel@chromium.org4860f052011-03-25 20:34:38 +000085def hack_subprocess():
86 """subprocess functions may throw exceptions when used in multiple threads.
87
88 See http://bugs.python.org/issue1731717 for more information.
89 """
90 global SUBPROCESS_CLEANUP_HACKED
91 if not SUBPROCESS_CLEANUP_HACKED and threading.activeCount() != 1:
92 # Only hack if there is ever multiple threads.
93 # There is no point to leak with only one thread.
94 subprocess._cleanup = lambda: None
95 SUBPROCESS_CLEANUP_HACKED = True
96
97
98def get_english_env(env):
99 """Forces LANG and/or LANGUAGE to be English.
100
101 Forces encoding to utf-8 for subprocesses.
102
103 Returns None if it is unnecessary.
104 """
105 env = env or os.environ
106
107 # Test if it is necessary at all.
108 is_english = lambda name: env.get(name, 'en').startswith('en')
109
110 if is_english('LANG') and is_english('LANGUAGE'):
111 return None
112
113 # Requires modifications.
114 env = env.copy()
115 def fix_lang(name):
116 if not is_english(name):
117 env[name] = 'en_US.UTF-8'
118 fix_lang('LANG')
119 fix_lang('LANGUAGE')
120 return env
121
122
123def Popen(args, **kwargs):
maruel@chromium.org5a376ed2011-03-30 01:18:15 +0000124 """Wraps subprocess.Popen().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000125
126 Forces English output since it's easier to parse the stdout if it is always in
127 English.
128
129 Sets shell=True on windows by default. You can override this by forcing shell
130 parameter to a value.
131
maruel@chromium.org5a376ed2011-03-30 01:18:15 +0000132 Popen() can throw OSError when cwd or args[0] doesn't exist.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000133 """
134 # Make sure we hack subprocess if necessary.
135 hack_subprocess()
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +0000136 add_kill()
maruel@chromium.org4860f052011-03-25 20:34:38 +0000137
138 env = get_english_env(kwargs.get('env'))
139 if env:
140 kwargs['env'] = env
141
142 if not kwargs.get('shell') is None:
143 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
144 # executable, but shell=True makes subprocess on Linux fail when it's called
145 # with a list because it only tries to execute the first item in the list.
146 kwargs['shell'] = (sys.platform=='win32')
147
148 tmp_str = ' '.join(args)
149 if kwargs.get('cwd', None):
150 tmp_str += '; cwd=%s' % kwargs['cwd']
151 logging.debug(tmp_str)
maruel@chromium.org5a376ed2011-03-30 01:18:15 +0000152 return subprocess.Popen(args, **kwargs)
maruel@chromium.org4860f052011-03-25 20:34:38 +0000153
154
155def call(args, timeout=None, **kwargs):
156 """Wraps subprocess.Popen().communicate().
157
158 The process will be kill with error code -9 after |timeout| seconds if set.
159
160 Automatically passes stdin content as input so do not specify stdin=PIPE.
161
162 Returns both communicate() tuple and return code wrapped in a tuple.
163 """
164 stdin = kwargs.pop('stdin', None)
165 if stdin is not None:
166 assert stdin != PIPE
167 # When stdin is passed as an argument, use it as the actual input data and
168 # set the Popen() parameter accordingly.
169 kwargs['stdin'] = PIPE
170
171 if not timeout:
172 # Normal workflow.
173 proc = Popen(args, **kwargs)
174 if stdin is not None:
175 out = proc.communicate(stdin)
176 else:
177 out = proc.communicate()
178 else:
179 # Create a temporary file to workaround python's deadlock.
180 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
181 # When the pipe fills up, it will deadlock this process. Using a real file
182 # works around that issue.
183 with tempfile.TemporaryFile() as buff:
184 start = time.time()
185 kwargs['stdout'] = buff
186 proc = Popen(args, **kwargs)
187 if stdin is not None:
188 proc.stdin.write(stdin)
189 while proc.returncode is None:
190 proc.poll()
191 if timeout and (time.time() - start) > timeout:
192 proc.kill()
193 proc.wait()
194 # It's -9 on linux and 1 on Windows. Standardize to -9.
195 # Do not throw an exception here, the user must use
196 # check_call(timeout=60) and check for e.returncode == -9 instead.
197 # or look at call()[1] == -9.
198 proc.returncode = -9
199 time.sleep(0.001)
200 # Now that the process died, reset the cursor and read the file.
201 buff.seek(0)
202 out = [buff.read(), None]
203 return out, proc.returncode
204
205
206def check_call(args, **kwargs):
207 """Similar to subprocess.check_call() but use call() instead.
208
209 This permits to include more details in CalledProcessError().
210
211 Runs a command and throws an exception if the command failed.
212
213 Returns communicate() tuple.
214 """
215 out, returncode = call(args, **kwargs)
216 if returncode:
217 raise CalledProcessError(
218 returncode, args, kwargs.get('cwd'), out[0], out[1])
219 return out
220
221
222def capture(args, **kwargs):
223 """Captures stdout of a process call and returns it.
224
225 Similar to check_output() excepts that it discards return code.
226
227 Discards communicate()[1]. By default sets stderr=STDOUT.
228 """
229 if kwargs.get('stderr') is None:
230 kwargs['stderr'] = STDOUT
231 return call(args, stdout=PIPE, **kwargs)[0][0]
232
233
234def check_output(args, **kwargs):
235 """Captures stdout of a process call and returns it.
236
237 Discards communicate()[1]. By default sets stderr=STDOUT.
238
239 Throws if return code is not 0.
240
241 Works even prior to python 2.7.
242 """
243 if kwargs.get('stderr') is None:
244 kwargs['stderr'] = STDOUT
245 return check_call(args, stdout=PIPE, **kwargs)[0]