blob: 3b48137988c3b2c529520940f0975d5a7ef7dbf3 [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.org1d9f6292011-04-07 14:15:36 +000011import errno
maruel@chromium.org4860f052011-03-25 20:34:38 +000012import logging
13import os
14import subprocess
15import sys
maruel@chromium.org7eda8622011-11-10 02:23:43 +000016import tempfile
maruel@chromium.org4860f052011-03-25 20:34:38 +000017import 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.
maruel@chromium.org0d5ef242011-04-18 13:52:58 +000024VOID = object()
maruel@chromium.org1d9f6292011-04-07 14:15:36 +000025# Error code when a process was killed because it timed out.
26TIMED_OUT = -2001
maruel@chromium.org4860f052011-03-25 20:34:38 +000027
28# Globals.
29# Set to True if you somehow need to disable this hack.
30SUBPROCESS_CLEANUP_HACKED = False
31
32
33class CalledProcessError(subprocess.CalledProcessError):
34 """Augment the standard exception with more data."""
35 def __init__(self, returncode, cmd, cwd, stdout, stderr):
36 super(CalledProcessError, self).__init__(returncode, cmd)
37 self.stdout = stdout
38 self.stderr = stderr
39 self.cwd = cwd
40
41 def __str__(self):
42 out = 'Command %s returned non-zero exit status %s' % (
43 ' '.join(self.cmd), self.returncode)
44 if self.cwd:
45 out += ' in ' + self.cwd
46 return '\n'.join(filter(None, (out, self.stdout, self.stderr)))
47
48
maruel@chromium.org1d9f6292011-04-07 14:15:36 +000049class CygwinRebaseError(CalledProcessError):
50 """Occurs when cygwin's fork() emulation fails due to rebased dll."""
51
52
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +000053## Utility functions
54
55
56def kill_pid(pid):
57 """Kills a process by its process id."""
58 try:
59 # Unable to import 'module'
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000060 # pylint: disable=E1101,F0401
maruel@chromium.orgfb3d3242011-04-01 14:03:08 +000061 import signal
62 return os.kill(pid, signal.SIGKILL)
63 except ImportError:
64 pass
65
66
67def kill_win(process):
68 """Kills a process with its windows handle.
69
70 Has no effect on other platforms.
71 """
72 try:
73 # Unable to import 'module'
74 # pylint: disable=F0401
75 import win32process
76 # Access to a protected member _handle of a client class
77 # pylint: disable=W0212
78 return win32process.TerminateProcess(process._handle, -1)
79 except ImportError:
80 pass
81
82
83def add_kill():
84 """Adds kill() method to subprocess.Popen for python <2.6"""
85 if hasattr(subprocess.Popen, 'kill'):
86 return
87
88 if sys.platform == 'win32':
89 subprocess.Popen.kill = kill_win
90 else:
91 subprocess.Popen.kill = lambda process: kill_pid(process.pid)
92
93
maruel@chromium.org4860f052011-03-25 20:34:38 +000094def hack_subprocess():
95 """subprocess functions may throw exceptions when used in multiple threads.
96
97 See http://bugs.python.org/issue1731717 for more information.
98 """
99 global SUBPROCESS_CLEANUP_HACKED
100 if not SUBPROCESS_CLEANUP_HACKED and threading.activeCount() != 1:
101 # Only hack if there is ever multiple threads.
102 # There is no point to leak with only one thread.
103 subprocess._cleanup = lambda: None
104 SUBPROCESS_CLEANUP_HACKED = True
105
106
107def get_english_env(env):
108 """Forces LANG and/or LANGUAGE to be English.
109
110 Forces encoding to utf-8 for subprocesses.
111
112 Returns None if it is unnecessary.
113 """
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +0000114 if sys.platform == 'win32':
115 return None
maruel@chromium.org4860f052011-03-25 20:34:38 +0000116 env = env or os.environ
117
118 # Test if it is necessary at all.
119 is_english = lambda name: env.get(name, 'en').startswith('en')
120
121 if is_english('LANG') and is_english('LANGUAGE'):
122 return None
123
124 # Requires modifications.
125 env = env.copy()
126 def fix_lang(name):
127 if not is_english(name):
128 env[name] = 'en_US.UTF-8'
129 fix_lang('LANG')
130 fix_lang('LANGUAGE')
131 return env
132
133
maruel@google.comef77f9e2011-11-24 15:24:02 +0000134class Popen(subprocess.Popen):
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000135 """Wraps subprocess.Popen() with various workarounds.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000136
maruel@chromium.org421982f2011-04-01 17:38:06 +0000137 - Forces English output since it's easier to parse the stdout if it is always
138 in English.
139 - Sets shell=True on windows by default. You can override this by forcing
140 shell parameter to a value.
141 - Adds support for VOID to not buffer when not needed.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000142
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000143 Note: Popen() can throw OSError when cwd or args[0] doesn't exist. Translate
144 exceptions generated by cygwin when it fails trying to emulate fork().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000145 """
maruel@google.comef77f9e2011-11-24 15:24:02 +0000146 def __init__(self, args, **kwargs):
147 # Make sure we hack subprocess if necessary.
148 hack_subprocess()
149 add_kill()
maruel@chromium.org4860f052011-03-25 20:34:38 +0000150
maruel@google.comef77f9e2011-11-24 15:24:02 +0000151 env = get_english_env(kwargs.get('env'))
152 if env:
153 kwargs['env'] = env
154 if kwargs.get('shell') is None:
155 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
156 # the executable, but shell=True makes subprocess on Linux fail when it's
157 # called with a list because it only tries to execute the first item in
158 # the list.
159 kwargs['shell'] = bool(sys.platform=='win32')
maruel@chromium.org4860f052011-03-25 20:34:38 +0000160
maruel@google.comef77f9e2011-11-24 15:24:02 +0000161 if isinstance(args, basestring):
162 tmp_str = args
163 elif isinstance(args, (list, tuple)):
164 tmp_str = ' '.join(args)
165 else:
166 raise CalledProcessError(None, args, kwargs.get('cwd'), None, None)
167 if kwargs.get('cwd', None):
168 tmp_str += '; cwd=%s' % kwargs['cwd']
169 logging.debug(tmp_str)
maruel@chromium.org421982f2011-04-01 17:38:06 +0000170
maruel@google.comef77f9e2011-11-24 15:24:02 +0000171 def fix(stream):
172 if kwargs.get(stream) in (VOID, os.devnull):
173 # Replaces VOID with handle to /dev/null.
174 # Create a temporary file to workaround python's deadlock.
175 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
176 # When the pipe fills up, it will deadlock this process. Using a real
177 # file works around that issue.
178 kwargs[stream] = open(os.devnull, 'w')
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000179
maruel@google.comef77f9e2011-11-24 15:24:02 +0000180 fix('stdout')
181 fix('stderr')
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000182
maruel@google.comef77f9e2011-11-24 15:24:02 +0000183 try:
184 super(Popen, self).__init__(args, **kwargs)
185 except OSError, e:
186 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
187 # Convert fork() emulation failure into a CygwinRebaseError().
188 raise CygwinRebaseError(
189 e.errno,
190 args,
191 kwargs.get('cwd'),
192 None,
193 'Visit '
194 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure '
195 'to learn how to fix this error; you need to rebase your cygwin '
196 'dlls')
197 # Popen() can throw OSError when cwd or args[0] doesn't exist. Let it go
198 # through
199 raise
maruel@chromium.org4860f052011-03-25 20:34:38 +0000200
201
maruel@chromium.org1f063db2011-04-18 19:04:52 +0000202def communicate(args, timeout=None, **kwargs):
maruel@chromium.org4860f052011-03-25 20:34:38 +0000203 """Wraps subprocess.Popen().communicate().
204
maruel@chromium.org421982f2011-04-01 17:38:06 +0000205 Returns ((stdout, stderr), returncode).
maruel@chromium.org4860f052011-03-25 20:34:38 +0000206
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000207 - The process will be killed after |timeout| seconds and returncode set to
208 TIMED_OUT.
maruel@chromium.org421982f2011-04-01 17:38:06 +0000209 - Automatically passes stdin content as input so do not specify stdin=PIPE.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000210 """
211 stdin = kwargs.pop('stdin', None)
212 if stdin is not None:
maruel@chromium.org0d5ef242011-04-18 13:52:58 +0000213 if stdin is VOID:
214 kwargs['stdin'] = open(os.devnull, 'r')
215 stdin = None
216 else:
maruel@chromium.org39f645f2011-04-21 00:07:53 +0000217 assert isinstance(stdin, basestring)
maruel@chromium.org0d5ef242011-04-18 13:52:58 +0000218 # When stdin is passed as an argument, use it as the actual input data and
219 # set the Popen() parameter accordingly.
220 kwargs['stdin'] = PIPE
maruel@chromium.org4860f052011-03-25 20:34:38 +0000221
maruel@chromium.org7eda8622011-11-10 02:23:43 +0000222 if not timeout:
maruel@chromium.org4860f052011-03-25 20:34:38 +0000223 # Normal workflow.
maruel@chromium.org7eda8622011-11-10 02:23:43 +0000224 proc = Popen(args, **kwargs)
225 if stdin is not None:
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000226 return proc.communicate(stdin), proc.returncode
maruel@chromium.org4860f052011-03-25 20:34:38 +0000227 else:
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000228 return proc.communicate(), proc.returncode
229
maruel@chromium.org7eda8622011-11-10 02:23:43 +0000230 # Create a temporary file to workaround python's deadlock.
maruel@chromium.org1d9f6292011-04-07 14:15:36 +0000231 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
maruel@chromium.org7eda8622011-11-10 02:23:43 +0000232 # When the pipe fills up, it will deadlock this process. Using a real file
233 # works around that issue.
234 with tempfile.TemporaryFile() as buff:
235 start = time.time()
236 kwargs['stdout'] = buff
237 proc = Popen(args, **kwargs)
238 if stdin is not None:
239 proc.stdin.write(stdin)
240 while proc.returncode is None:
241 proc.poll()
242 if timeout and (time.time() - start) > timeout:
243 proc.kill()
244 proc.wait()
245 # It's -9 on linux and 1 on Windows. Standardize to TIMED_OUT.
246 proc.returncode = TIMED_OUT
247 time.sleep(0.001)
248 # Now that the process died, reset the cursor and read the file.
249 buff.seek(0)
maruel@chromium.org4942e4a2011-11-15 15:50:50 +0000250 out = (buff.read(), None)
maruel@chromium.org7eda8622011-11-10 02:23:43 +0000251 return out, proc.returncode
maruel@chromium.org4860f052011-03-25 20:34:38 +0000252
253
maruel@chromium.org1f063db2011-04-18 19:04:52 +0000254def call(args, **kwargs):
255 """Emulates subprocess.call().
256
257 Automatically convert stdout=PIPE or stderr=PIPE to VOID.
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000258 In no case they can be returned since no code path raises
259 subprocess2.CalledProcessError.
maruel@chromium.org1f063db2011-04-18 19:04:52 +0000260 """
261 if kwargs.get('stdout') == PIPE:
262 kwargs['stdout'] = VOID
263 if kwargs.get('stderr') == PIPE:
264 kwargs['stderr'] = VOID
265 return communicate(args, **kwargs)[1]
266
267
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000268def check_call_out(args, **kwargs):
maruel@chromium.org421982f2011-04-01 17:38:06 +0000269 """Improved version of subprocess.check_call().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000270
maruel@chromium.org421982f2011-04-01 17:38:06 +0000271 Returns (stdout, stderr), unlike subprocess.check_call().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000272 """
maruel@chromium.org1f063db2011-04-18 19:04:52 +0000273 out, returncode = communicate(args, **kwargs)
maruel@chromium.org4860f052011-03-25 20:34:38 +0000274 if returncode:
275 raise CalledProcessError(
276 returncode, args, kwargs.get('cwd'), out[0], out[1])
277 return out
278
279
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000280def check_call(args, **kwargs):
281 """Emulate subprocess.check_call()."""
282 check_call_out(args, **kwargs)
283 return 0
284
285
maruel@chromium.org4860f052011-03-25 20:34:38 +0000286def capture(args, **kwargs):
287 """Captures stdout of a process call and returns it.
288
maruel@chromium.org421982f2011-04-01 17:38:06 +0000289 Returns stdout.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000290
maruel@chromium.org421982f2011-04-01 17:38:06 +0000291 - Discards returncode.
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000292 - Blocks stdin by default if not specified since no output will be visible.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000293 """
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000294 kwargs.setdefault('stdin', VOID)
295
296 # Like check_output, deny the caller from using stdout arg.
297 return communicate(args, stdout=PIPE, **kwargs)[0][0]
maruel@chromium.org4860f052011-03-25 20:34:38 +0000298
299
300def check_output(args, **kwargs):
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000301 """Emulates subprocess.check_output().
maruel@chromium.org4860f052011-03-25 20:34:38 +0000302
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000303 Captures stdout of a process call and returns stdout only.
maruel@chromium.org4860f052011-03-25 20:34:38 +0000304
maruel@chromium.org421982f2011-04-01 17:38:06 +0000305 - Throws if return code is not 0.
306 - Works even prior to python 2.7.
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000307 - Blocks stdin by default if not specified since no output will be visible.
308 - As per doc, "The stdout argument is not allowed as it is used internally."
maruel@chromium.org4860f052011-03-25 20:34:38 +0000309 """
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000310 kwargs.setdefault('stdin', VOID)
311 return check_call_out(args, stdout=PIPE, **kwargs)[0]