blob: ed34d3e98eebfa0c5bf7185db6d3a83d04505916 [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
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.org5a376ed2011-03-30 01:18:15 +000083 """Wraps subprocess.Popen().
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.org5a376ed2011-03-30 01:18:15 +000091 Popen() can throw OSError when cwd or args[0] doesn't exist.
maruel@chromium.org4860f052011-03-25 20:34:38 +000092 """
93 # Make sure we hack subprocess if necessary.
94 hack_subprocess()
95
96 env = get_english_env(kwargs.get('env'))
97 if env:
98 kwargs['env'] = env
99
100 if not kwargs.get('shell') is None:
101 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
102 # executable, but shell=True makes subprocess on Linux fail when it's called
103 # with a list because it only tries to execute the first item in the list.
104 kwargs['shell'] = (sys.platform=='win32')
105
106 tmp_str = ' '.join(args)
107 if kwargs.get('cwd', None):
108 tmp_str += '; cwd=%s' % kwargs['cwd']
109 logging.debug(tmp_str)
maruel@chromium.org5a376ed2011-03-30 01:18:15 +0000110 return subprocess.Popen(args, **kwargs)
maruel@chromium.org4860f052011-03-25 20:34:38 +0000111
112
113def call(args, timeout=None, **kwargs):
114 """Wraps subprocess.Popen().communicate().
115
116 The process will be kill with error code -9 after |timeout| seconds if set.
117
118 Automatically passes stdin content as input so do not specify stdin=PIPE.
119
120 Returns both communicate() tuple and return code wrapped in a tuple.
121 """
122 stdin = kwargs.pop('stdin', None)
123 if stdin is not None:
124 assert stdin != PIPE
125 # When stdin is passed as an argument, use it as the actual input data and
126 # set the Popen() parameter accordingly.
127 kwargs['stdin'] = PIPE
128
129 if not timeout:
130 # Normal workflow.
131 proc = Popen(args, **kwargs)
132 if stdin is not None:
133 out = proc.communicate(stdin)
134 else:
135 out = proc.communicate()
136 else:
137 # Create a temporary file to workaround python's deadlock.
138 # http://docs.python.org/library/subprocess.html#subprocess.Popen.wait
139 # When the pipe fills up, it will deadlock this process. Using a real file
140 # works around that issue.
141 with tempfile.TemporaryFile() as buff:
142 start = time.time()
143 kwargs['stdout'] = buff
144 proc = Popen(args, **kwargs)
145 if stdin is not None:
146 proc.stdin.write(stdin)
147 while proc.returncode is None:
148 proc.poll()
149 if timeout and (time.time() - start) > timeout:
150 proc.kill()
151 proc.wait()
152 # It's -9 on linux and 1 on Windows. Standardize to -9.
153 # Do not throw an exception here, the user must use
154 # check_call(timeout=60) and check for e.returncode == -9 instead.
155 # or look at call()[1] == -9.
156 proc.returncode = -9
157 time.sleep(0.001)
158 # Now that the process died, reset the cursor and read the file.
159 buff.seek(0)
160 out = [buff.read(), None]
161 return out, proc.returncode
162
163
164def check_call(args, **kwargs):
165 """Similar to subprocess.check_call() but use call() instead.
166
167 This permits to include more details in CalledProcessError().
168
169 Runs a command and throws an exception if the command failed.
170
171 Returns communicate() tuple.
172 """
173 out, returncode = call(args, **kwargs)
174 if returncode:
175 raise CalledProcessError(
176 returncode, args, kwargs.get('cwd'), out[0], out[1])
177 return out
178
179
180def capture(args, **kwargs):
181 """Captures stdout of a process call and returns it.
182
183 Similar to check_output() excepts that it discards return code.
184
185 Discards communicate()[1]. By default sets stderr=STDOUT.
186 """
187 if kwargs.get('stderr') is None:
188 kwargs['stderr'] = STDOUT
189 return call(args, stdout=PIPE, **kwargs)[0][0]
190
191
192def check_output(args, **kwargs):
193 """Captures stdout of a process call and returns it.
194
195 Discards communicate()[1]. By default sets stderr=STDOUT.
196
197 Throws if return code is not 0.
198
199 Works even prior to python 2.7.
200 """
201 if kwargs.get('stderr') is None:
202 kwargs['stderr'] = STDOUT
203 return check_call(args, stdout=PIPE, **kwargs)[0]