blob: 9aefeea45702ca2ab177593539acf4ab1c240991 [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.org24254942011-03-30 01:05:04 +000010import errno
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
44def hack_subprocess():
45 """subprocess functions may throw exceptions when used in multiple threads.
46
47 See http://bugs.python.org/issue1731717 for more information.
48 """
49 global SUBPROCESS_CLEANUP_HACKED
50 if not SUBPROCESS_CLEANUP_HACKED and threading.activeCount() != 1:
51 # Only hack if there is ever multiple threads.
52 # There is no point to leak with only one thread.
53 subprocess._cleanup = lambda: None
54 SUBPROCESS_CLEANUP_HACKED = True
55
56
57def get_english_env(env):
58 """Forces LANG and/or LANGUAGE to be English.
59
60 Forces encoding to utf-8 for subprocesses.
61
62 Returns None if it is unnecessary.
63 """
64 env = env or os.environ
65
66 # Test if it is necessary at all.
67 is_english = lambda name: env.get(name, 'en').startswith('en')
68
69 if is_english('LANG') and is_english('LANGUAGE'):
70 return None
71
72 # Requires modifications.
73 env = env.copy()
74 def fix_lang(name):
75 if not is_english(name):
76 env[name] = 'en_US.UTF-8'
77 fix_lang('LANG')
78 fix_lang('LANGUAGE')
79 return env
80
81
82def Popen(args, **kwargs):
maruel@chromium.org24254942011-03-30 01:05:04 +000083 """Wraps subprocess.Popen() with various workarounds.
maruel@chromium.org4860f052011-03-25 20:34:38 +000084
85 Forces English output since it's easier to parse the stdout if it is always in
86 English.
87
88 Sets shell=True on windows by default. You can override this by forcing shell
89 parameter to a value.
90
maruel@chromium.org24254942011-03-30 01:05:04 +000091 Popen() can throw OSError when cwd or args[0] doesn't exist. Translate
92 exceptions generated by cygwin when it fails trying to emulate fork().
maruel@chromium.org4860f052011-03-25 20:34:38 +000093 """
94 # Make sure we hack subprocess if necessary.
95 hack_subprocess()
96
97 env = get_english_env(kwargs.get('env'))
98 if env:
99 kwargs['env'] = env
100
101 if not kwargs.get('shell') is None:
102 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
103 # executable, but shell=True makes subprocess on Linux fail when it's called
104 # with a list because it only tries to execute the first item in the list.
105 kwargs['shell'] = (sys.platform=='win32')
106
107 tmp_str = ' '.join(args)
108 if kwargs.get('cwd', None):
109 tmp_str += '; cwd=%s' % kwargs['cwd']
110 logging.debug(tmp_str)
maruel@chromium.org24254942011-03-30 01:05:04 +0000111 try:
112 return subprocess.Popen(args, **kwargs)
113 except OSError, e:
114 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
115 # Convert fork() emulation failure into a CalledProcessError().
116 raise CalledProcessError(
117 e.errno,
118 args,
119 kwargs.get('cwd'),
120 'Visit '
121 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
122 'learn how to fix this error; you need to rebase your cygwin dlls',
123 None)
124 raise
maruel@chromium.org4860f052011-03-25 20:34:38 +0000125
126
127def call(args, timeout=None, **kwargs):
128 """Wraps subprocess.Popen().communicate().
129
130 The process will be kill with error code -9 after |timeout| seconds if set.
131
132 Automatically passes stdin content as input so do not specify stdin=PIPE.
133
134 Returns both communicate() tuple and return code wrapped in a tuple.
135 """
136 stdin = kwargs.pop('stdin', None)
137 if stdin is not None:
138 assert stdin != PIPE
139 # When stdin is passed as an argument, use it as the actual input data and
140 # set the Popen() parameter accordingly.
141 kwargs['stdin'] = PIPE
142
143 if not timeout:
144 # Normal workflow.
145 proc = Popen(args, **kwargs)
146 if stdin is not None:
147 out = proc.communicate(stdin)
148 else:
149 out = proc.communicate()
150 else:
151 # Create a temporary file to workaround python's deadlock.
152 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
153 # When the pipe fills up, it will deadlock this process. Using a real file
154 # works around that issue.
155 with tempfile.TemporaryFile() as buff:
156 start = time.time()
157 kwargs['stdout'] = buff
158 proc = Popen(args, **kwargs)
159 if stdin is not None:
160 proc.stdin.write(stdin)
161 while proc.returncode is None:
162 proc.poll()
163 if timeout and (time.time() - start) > timeout:
164 proc.kill()
165 proc.wait()
166 # It's -9 on linux and 1 on Windows. Standardize to -9.
167 # Do not throw an exception here, the user must use
168 # check_call(timeout=60) and check for e.returncode == -9 instead.
169 # or look at call()[1] == -9.
170 proc.returncode = -9
171 time.sleep(0.001)
172 # Now that the process died, reset the cursor and read the file.
173 buff.seek(0)
174 out = [buff.read(), None]
175 return out, proc.returncode
176
177
178def check_call(args, **kwargs):
179 """Similar to subprocess.check_call() but use call() instead.
180
181 This permits to include more details in CalledProcessError().
182
183 Runs a command and throws an exception if the command failed.
184
185 Returns communicate() tuple.
186 """
187 out, returncode = call(args, **kwargs)
188 if returncode:
189 raise CalledProcessError(
190 returncode, args, kwargs.get('cwd'), out[0], out[1])
191 return out
192
193
194def capture(args, **kwargs):
195 """Captures stdout of a process call and returns it.
196
197 Similar to check_output() excepts that it discards return code.
198
199 Discards communicate()[1]. By default sets stderr=STDOUT.
200 """
201 if kwargs.get('stderr') is None:
202 kwargs['stderr'] = STDOUT
203 return call(args, stdout=PIPE, **kwargs)[0][0]
204
205
206def check_output(args, **kwargs):
207 """Captures stdout of a process call and returns it.
208
209 Discards communicate()[1]. By default sets stderr=STDOUT.
210
211 Throws if return code is not 0.
212
213 Works even prior to python 2.7.
214 """
215 if kwargs.get('stderr') is None:
216 kwargs['stderr'] = STDOUT
217 return check_call(args, stdout=PIPE, **kwargs)[0]