blob: d4fa5781d6ee1be9f99b14ab17c156f2b02f1260 [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.org93ef4102011-04-01 20:37:02 +000011import errno
maruel@chromium.org4860f052011-03-25 20:34:38 +000012import logging
13import os
14import subprocess
15import sys
16import tempfile
17import time
18import threading
19
20# Constants forwarded from subprocess.
21PIPE = subprocess.PIPE
22STDOUT = subprocess.STDOUT
maruel@chromium.org421982f2011-04-01 17:38:06 +000023# Sends stdout or stderr to os.devnull.
24VOID = '/dev/null'
25
maruel@chromium.org4860f052011-03-25 20:34:38 +000026
27# Globals.
28# Set to True if you somehow need to disable this hack.
29SUBPROCESS_CLEANUP_HACKED = False
30
31
32class CalledProcessError(subprocess.CalledProcessError):
33 """Augment the standard exception with more data."""
34 def __init__(self, returncode, cmd, cwd, stdout, stderr):
35 super(CalledProcessError, self).__init__(returncode, cmd)
36 self.stdout = stdout
37 self.stderr = stderr
38 self.cwd = cwd
39
40 def __str__(self):
41 out = 'Command %s returned non-zero exit status %s' % (
42 ' '.join(self.cmd), self.returncode)
43 if self.cwd:
44 out += ' in ' + self.cwd
45 return '\n'.join(filter(None, (out, self.stdout, self.stderr)))
46
47
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +000048## Utility functions
49
50
51def kill_pid(pid):
52 """Kills a process by its process id."""
53 try:
54 # Unable to import 'module'
55 # pylint: disable=F0401
56 import signal
57 return os.kill(pid, signal.SIGKILL)
58 except ImportError:
59 pass
60
61
62def kill_win(process):
63 """Kills a process with its windows handle.
64
65 Has no effect on other platforms.
66 """
67 try:
68 # Unable to import 'module'
69 # pylint: disable=F0401
70 import win32process
71 # Access to a protected member _handle of a client class
72 # pylint: disable=W0212
73 return win32process.TerminateProcess(process._handle, -1)
74 except ImportError:
75 pass
76
77
78def add_kill():
79 """Adds kill() method to subprocess.Popen for python <2.6"""
80 if hasattr(subprocess.Popen, 'kill'):
81 return
82
83 if sys.platform == 'win32':
84 subprocess.Popen.kill = kill_win
85 else:
86 subprocess.Popen.kill = lambda process: kill_pid(process.pid)
87
88
maruel@chromium.org4860f052011-03-25 20:34:38 +000089def hack_subprocess():
90 """subprocess functions may throw exceptions when used in multiple threads.
91
92 See http://bugs.python.org/issue1731717 for more information.
93 """
94 global SUBPROCESS_CLEANUP_HACKED
95 if not SUBPROCESS_CLEANUP_HACKED and threading.activeCount() != 1:
96 # Only hack if there is ever multiple threads.
97 # There is no point to leak with only one thread.
98 subprocess._cleanup = lambda: None
99 SUBPROCESS_CLEANUP_HACKED = True
100
101
102def get_english_env(env):
103 """Forces LANG and/or LANGUAGE to be English.
104
105 Forces encoding to utf-8 for subprocesses.
106
107 Returns None if it is unnecessary.
108 """
109 env = env or os.environ
110
111 # Test if it is necessary at all.
112 is_english = lambda name: env.get(name, 'en').startswith('en')
113
114 if is_english('LANG') and is_english('LANGUAGE'):
115 return None
116
117 # Requires modifications.
118 env = env.copy()
119 def fix_lang(name):
120 if not is_english(name):
121 env[name] = 'en_US.UTF-8'
122 fix_lang('LANG')
123 fix_lang('LANGUAGE')
124 return env
125
126
127def Popen(args, **kwargs):
maruel@chromium.org93ef4102011-04-01 20:37:02 +0000128 """Wraps subprocess.Popen() with various workarounds.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000129
maruel@chromium.org421982f2011-04-01 17:38:06 +0000130 Returns a subprocess.Popen object.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000131
maruel@chromium.org421982f2011-04-01 17:38:06 +0000132 - Forces English output since it's easier to parse the stdout if it is always
133 in English.
134 - Sets shell=True on windows by default. You can override this by forcing
135 shell parameter to a value.
136 - Adds support for VOID to not buffer when not needed.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000137
maruel@chromium.org93ef4102011-04-01 20:37:02 +0000138 Note: Popen() can throw OSError when cwd or args[0] doesn't exist. Translate
139 exceptions generated by cygwin when it fails trying to emulate fork().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000140 """
141 # Make sure we hack subprocess if necessary.
142 hack_subprocess()
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +0000143 add_kill()
maruel@chromium.org4860f052011-03-25 20:34:38 +0000144
145 env = get_english_env(kwargs.get('env'))
146 if env:
147 kwargs['env'] = env
148
149 if not kwargs.get('shell') is None:
150 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
151 # executable, but shell=True makes subprocess on Linux fail when it's called
152 # with a list because it only tries to execute the first item in the list.
153 kwargs['shell'] = (sys.platform=='win32')
154
155 tmp_str = ' '.join(args)
156 if kwargs.get('cwd', None):
157 tmp_str += '; cwd=%s' % kwargs['cwd']
158 logging.debug(tmp_str)
maruel@chromium.org421982f2011-04-01 17:38:06 +0000159
160 # Replaces VOID with handle to /dev/null.
161 if kwargs.get('stdout') in (VOID, os.devnull):
162 kwargs['stdout'] = open(os.devnull, 'w')
163 if kwargs.get('stderr') in (VOID, os.devnull):
164 kwargs['stderr'] = open(os.devnull, 'w')
maruel@chromium.org93ef4102011-04-01 20:37:02 +0000165 try:
166 return subprocess.Popen(args, **kwargs)
167 except OSError, e:
168 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
169 # Convert fork() emulation failure into a CalledProcessError().
170 raise CalledProcessError(
171 e.errno,
172 args,
173 kwargs.get('cwd'),
174 'Visit '
175 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
176 'learn how to fix this error; you need to rebase your cygwin dlls',
177 None)
178 raise
maruel@chromium.org4860f052011-03-25 20:34:38 +0000179
180
181def call(args, timeout=None, **kwargs):
182 """Wraps subprocess.Popen().communicate().
183
maruel@chromium.org421982f2011-04-01 17:38:06 +0000184 Returns ((stdout, stderr), returncode).
maruel@chromium.org4860f052011-03-25 20:34:38 +0000185
maruel@chromium.org421982f2011-04-01 17:38:06 +0000186 - The process will be kill with error code -9 after |timeout| seconds if set.
187 - Automatically passes stdin content as input so do not specify stdin=PIPE.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000188 """
189 stdin = kwargs.pop('stdin', None)
190 if stdin is not None:
191 assert stdin != PIPE
192 # When stdin is passed as an argument, use it as the actual input data and
193 # set the Popen() parameter accordingly.
194 kwargs['stdin'] = PIPE
195
196 if not timeout:
197 # Normal workflow.
198 proc = Popen(args, **kwargs)
199 if stdin is not None:
200 out = proc.communicate(stdin)
201 else:
202 out = proc.communicate()
203 else:
204 # Create a temporary file to workaround python's deadlock.
205 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
206 # When the pipe fills up, it will deadlock this process. Using a real file
207 # works around that issue.
208 with tempfile.TemporaryFile() as buff:
209 start = time.time()
210 kwargs['stdout'] = buff
211 proc = Popen(args, **kwargs)
212 if stdin is not None:
213 proc.stdin.write(stdin)
214 while proc.returncode is None:
215 proc.poll()
216 if timeout and (time.time() - start) > timeout:
217 proc.kill()
218 proc.wait()
219 # It's -9 on linux and 1 on Windows. Standardize to -9.
220 # Do not throw an exception here, the user must use
221 # check_call(timeout=60) and check for e.returncode == -9 instead.
222 # or look at call()[1] == -9.
223 proc.returncode = -9
224 time.sleep(0.001)
225 # Now that the process died, reset the cursor and read the file.
226 buff.seek(0)
227 out = [buff.read(), None]
228 return out, proc.returncode
229
230
231def check_call(args, **kwargs):
maruel@chromium.org421982f2011-04-01 17:38:06 +0000232 """Improved version of subprocess.check_call().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000233
maruel@chromium.org421982f2011-04-01 17:38:06 +0000234 Returns (stdout, stderr), unlike subprocess.check_call().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000235 """
236 out, returncode = call(args, **kwargs)
237 if returncode:
238 raise CalledProcessError(
239 returncode, args, kwargs.get('cwd'), out[0], out[1])
240 return out
241
242
243def capture(args, **kwargs):
244 """Captures stdout of a process call and returns it.
245
maruel@chromium.org421982f2011-04-01 17:38:06 +0000246 Returns stdout.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000247
maruel@chromium.org421982f2011-04-01 17:38:06 +0000248 - Discards returncode.
249 - Discards stderr. By default sets stderr=STDOUT.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000250 """
251 if kwargs.get('stderr') is None:
252 kwargs['stderr'] = STDOUT
253 return call(args, stdout=PIPE, **kwargs)[0][0]
254
255
256def check_output(args, **kwargs):
257 """Captures stdout of a process call and returns it.
258
maruel@chromium.org421982f2011-04-01 17:38:06 +0000259 Returns stdout.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000260
maruel@chromium.org421982f2011-04-01 17:38:06 +0000261 - Discards stderr. By default sets stderr=STDOUT.
262 - Throws if return code is not 0.
263 - Works even prior to python 2.7.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000264 """
265 if kwargs.get('stderr') is None:
266 kwargs['stderr'] = STDOUT
267 return check_call(args, stdout=PIPE, **kwargs)[0]