1 # Copyright (c) 2009, Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
14 # * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 import multiprocessing
40 from webkitpy.common.system.outputtee import Tee
41 from webkitpy.common.system.filesystem import FileSystem
44 _log = logging.getLogger(__name__)
47 class ScriptError(Exception):
56 shortened_output = output
57 if output and output_limit and len(output) > output_limit:
58 shortened_output = "Last %s characters of output:\n%s" % (output_limit, output[-output_limit:])
61 message = 'Failed to run "%s"' % repr(script_args)
63 message += " exit_code: %d" % exit_code
65 message += " cwd: %s" % cwd
68 message += "\n\noutput: %s" % shortened_output
70 Exception.__init__(self, message)
71 self.script_args = script_args # 'args' is already used by Exception
72 self.exit_code = exit_code
76 def message_with_output(self):
79 def command_name(self):
80 command_path = self.script_args
81 if type(command_path) is list:
82 command_path = command_path[0]
83 return os.path.basename(command_path)
86 class Executive(object):
87 PIPE = subprocess.PIPE
88 STDOUT = subprocess.STDOUT
90 def _should_close_fds(self):
91 # We need to pass close_fds=True to work around Python bug #2320
92 # (otherwise we can hang when we kill DumpRenderTree when we are running
93 # multiple threads). See http://bugs.python.org/issue2320 .
94 # Note that close_fds isn't supported on Windows, but this bug only
95 # shows up on Mac and Linux.
96 return sys.platform not in ('win32', 'cygwin')
98 def _run_command_with_teed_output(self, args, teed_output, **kwargs):
99 child_process = self.popen(args,
102 close_fds=self._should_close_fds(),
105 # Use our own custom wait loop because Popen ignores a tee'd
107 # FIXME: This could be improved not to flatten output to stdout.
109 output_line = child_process.stdout.readline()
110 if output_line == "" and child_process.poll() != None:
111 # poll() is not threadsafe and can throw OSError due to:
112 # http://bugs.python.org/issue1731717
113 return child_process.poll()
114 # We assume that the child process wrote to us in utf-8,
115 # so no re-encoding is necessary before writing here.
116 teed_output.write(output_line)
118 # FIXME: Remove this deprecated method and move callers to run_command.
119 # FIXME: This method is a hack to allow running command which both
120 # capture their output and print out to stdin. Useful for things
121 # like "build-webkit" where we want to display to the user that we're building
122 # but still have the output to stuff into a log file.
123 def run_and_throw_if_fail(self, args, quiet=False, decode_output=True, **kwargs):
124 # Cache the child's output locally so it can be used for error reports.
125 child_out_file = StringIO.StringIO()
126 tee_stdout = sys.stdout
128 dev_null = open(os.devnull, "w") # FIXME: Does this need an encoding?
129 tee_stdout = dev_null
130 child_stdout = Tee(child_out_file, tee_stdout)
131 exit_code = self._run_command_with_teed_output(args, child_stdout, **kwargs)
135 child_output = child_out_file.getvalue()
136 child_out_file.close()
139 child_output = child_output.decode(self._child_process_encoding())
142 raise ScriptError(script_args=args,
148 return multiprocessing.cpu_count()
151 def interpreter_for_script(script_path, fs=None):
152 fs = fs or FileSystem()
153 lines = fs.read_text_file(script_path).splitlines()
156 first_line = lines[0]
157 if not first_line.startswith('#!'):
159 if first_line.find('python') > -1:
160 return sys.executable
161 if first_line.find('perl') > -1:
163 if first_line.find('ruby') > -1:
168 def shell_command_for_script(script_path, fs=None):
169 fs = fs or FileSystem()
170 # Win32 does not support shebang. We need to detect the interpreter ourself.
171 if sys.platform == 'win32':
172 interpreter = Executive.interpreter_for_script(script_path, fs)
174 return [interpreter, script_path]
177 def kill_process(self, pid):
178 """Attempts to kill the given pid.
179 Will fail silently if pid does not exist or insufficient permisssions."""
180 if sys.platform == "win32":
181 # We only use taskkill.exe on windows (not cygwin) because subprocess.pid
182 # is a CYGWIN pid and taskkill.exe expects a windows pid.
183 # Thankfully os.kill on CYGWIN handles either pid type.
184 command = ["taskkill.exe", "/f", "/t", "/pid", pid]
185 # taskkill will exit 128 if the process is not found. We should log.
186 self.run_command(command, error_handler=self.ignore_error)
189 # According to http://docs.python.org/library/os.html
190 # os.kill isn't available on Windows. python 2.5.5 os.kill appears
191 # to work in cygwin, however it occasionally raises EAGAIN.
192 retries_left = 10 if sys.platform == "cygwin" else 1
193 while retries_left > 0:
196 os.kill(pid, signal.SIGKILL)
197 _ = os.waitpid(pid, os.WNOHANG)
199 if e.errno == errno.EAGAIN:
200 if retries_left <= 0:
201 _log.warn("Failed to kill pid %s. Too many EAGAIN errors." % pid)
203 if e.errno == errno.ESRCH: # The process does not exist.
205 if e.errno == errno.EPIPE: # The process has exited already on cygwin
207 if e.errno == errno.ECHILD:
208 # Can't wait on a non-child process, but the kill worked.
210 if e.errno == errno.EACCES and sys.platform == 'cygwin':
211 # Cygwin python sometimes can't kill native processes.
215 def _win32_check_running_pid(self, pid):
216 # importing ctypes at the top-level seems to cause weird crashes at
217 # exit under cygwin on apple's win port. Only win32 needs cygwin, so
218 # we import it here instead. See https://bugs.webkit.org/show_bug.cgi?id=91682
221 class PROCESSENTRY32(ctypes.Structure):
222 _fields_ = [("dwSize", ctypes.c_ulong),
223 ("cntUsage", ctypes.c_ulong),
224 ("th32ProcessID", ctypes.c_ulong),
225 ("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)),
226 ("th32ModuleID", ctypes.c_ulong),
227 ("cntThreads", ctypes.c_ulong),
228 ("th32ParentProcessID", ctypes.c_ulong),
229 ("pcPriClassBase", ctypes.c_ulong),
230 ("dwFlags", ctypes.c_ulong),
231 ("szExeFile", ctypes.c_char * 260)]
233 CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
234 Process32First = ctypes.windll.kernel32.Process32First
235 Process32Next = ctypes.windll.kernel32.Process32Next
236 CloseHandle = ctypes.windll.kernel32.CloseHandle
237 TH32CS_SNAPPROCESS = 0x00000002 # win32 magic number
238 hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
239 pe32 = PROCESSENTRY32()
240 pe32.dwSize = ctypes.sizeof(PROCESSENTRY32)
242 if not Process32First(hProcessSnap, ctypes.byref(pe32)):
243 _log.debug("Failed getting first process.")
244 CloseHandle(hProcessSnap)
247 if pe32.th32ProcessID == pid:
250 if not Process32Next(hProcessSnap, ctypes.byref(pe32)):
252 CloseHandle(hProcessSnap)
255 def check_running_pid(self, pid):
256 """Return True if pid is alive, otherwise return False."""
257 if sys.platform == 'win32':
258 return self._win32_check_running_pid(pid)
266 def running_pids(self, process_name_filter=None):
267 if not process_name_filter:
268 process_name_filter = lambda process_name: True
272 if sys.platform in ("win32", "cygwin"):
273 # FIXME: running_pids isn't implemented on Windows yet...
276 ps_process = self.popen(['ps', '-eo', 'pid,comm'], stdout=self.PIPE, stderr=self.PIPE)
277 stdout, _ = ps_process.communicate()
278 for line in stdout.splitlines():
280 # In some cases the line can contain one or more
281 # leading white-spaces, so strip it before split.
282 pid, process_name = line.strip().split(' ', 1)
283 if process_name_filter(process_name):
284 running_pids.append(int(pid))
285 except ValueError, e:
288 return sorted(running_pids)
290 def wait_newest(self, process_name_filter=None):
291 if not process_name_filter:
292 process_name_filter = lambda process_name: True
294 running_pids = self.running_pids(process_name_filter)
297 pid = running_pids[-1]
299 while self.check_running_pid(pid):
302 def wait_limited(self, pid, limit_in_seconds=None, check_frequency_in_seconds=None):
303 seconds_left = limit_in_seconds or 10
304 sleep_length = check_frequency_in_seconds or 1
305 while seconds_left > 0 and self.check_running_pid(pid):
306 seconds_left -= sleep_length
307 time.sleep(sleep_length)
309 def _windows_image_name(self, process_name):
310 name, extension = os.path.splitext(process_name)
312 # taskkill expects processes to end in .exe
313 # If necessary we could add a flag to disable appending .exe.
314 process_name = "%s.exe" % name
317 def interrupt(self, pid):
318 interrupt_signal = signal.SIGINT
319 # FIXME: The python docs seem to imply that platform == 'win32' may need to use signal.CTRL_C_EVENT
320 # http://docs.python.org/2/library/signal.html
322 os.kill(pid, interrupt_signal)
324 # Silently ignore when the pid doesn't exist.
325 # It's impossible for callers to avoid race conditions with process shutdown.
328 # Error handlers do not need to be static methods once all callers are
329 # updated to use an Executive object.
332 def default_error_handler(error):
336 def ignore_error(error):
339 def _compute_stdin(self, input):
340 """Returns (stdin, string_to_communicate)"""
341 # FIXME: We should be returning /dev/null for stdin
342 # or closing stdin after process creation to prevent
343 # child processes from getting input from the user.
346 if hasattr(input, "read"): # Check if the input is a file.
347 return (input, None) # Assume the file is in the right encoding.
349 # Popen in Python 2.5 and before does not automatically encode unicode objects.
350 # http://bugs.python.org/issue5290
351 # See https://bugs.webkit.org/show_bug.cgi?id=37528
352 # for an example of a regresion caused by passing a unicode string directly.
353 # FIXME: We may need to encode differently on different platforms.
354 if isinstance(input, unicode):
355 input = input.encode(self._child_process_encoding())
356 return (self.PIPE, input)
358 def command_for_printing(self, args):
359 """Returns a print-ready string representing command args.
360 The string should be copy/paste ready for execution in a shell."""
361 args = self._stringify_args(args)
364 if isinstance(arg, unicode):
365 # Escape any non-ascii characters for easy copy/paste
366 arg = arg.encode("unicode_escape")
367 # FIXME: Do we need to fix quotes here?
368 escaped_args.append(arg)
369 return " ".join(escaped_args)
371 # FIXME: run_and_throw_if_fail should be merged into this method.
372 def run_command(self,
378 return_exit_code=False,
380 decode_output=True, debug_logging=True):
381 """Popen wrapper for convenience and to work around python bugs."""
382 assert(isinstance(args, list) or isinstance(args, tuple))
383 start_time = time.time()
385 stdin, string_to_communicate = self._compute_stdin(input)
386 stderr = self.STDOUT if return_stderr else None
388 process = self.popen(args,
394 close_fds=self._should_close_fds())
395 output = process.communicate(string_to_communicate)[0]
397 # run_command automatically decodes to unicode() unless explicitly told not to.
399 output = output.decode(self._child_process_encoding())
401 # wait() is not threadsafe and can throw OSError due to:
402 # http://bugs.python.org/issue1731717
403 exit_code = process.wait()
406 _log.debug('"%s" took %.2fs' % (self.command_for_printing(args), time.time() - start_time))
412 script_error = ScriptError(script_args=args,
416 (error_handler or self.default_error_handler)(script_error)
419 def _child_process_encoding(self):
420 # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
421 # to launch subprocesses, so we have to encode arguments using the
423 if sys.platform == 'win32' and sys.version < '3':
425 # All other platforms use UTF-8.
426 # FIXME: Using UTF-8 on Cygwin will confuse Windows-native commands
427 # which will expect arguments to be encoded using the current code
431 def _should_encode_child_process_arguments(self):
432 # Cygwin's Python's os.execv doesn't support unicode command
433 # arguments, and neither does Cygwin's execv itself.
434 if sys.platform == 'cygwin':
437 # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
438 # to launch subprocesses, so we have to encode arguments using the
440 if sys.platform == 'win32' and sys.version < '3':
445 def _encode_argument_if_needed(self, argument):
446 if not self._should_encode_child_process_arguments():
448 return argument.encode(self._child_process_encoding())
450 def _stringify_args(self, args):
451 # Popen will throw an exception if args are non-strings (like int())
452 string_args = map(unicode, args)
453 # The Windows implementation of Popen cannot handle unicode strings. :(
454 return map(self._encode_argument_if_needed, string_args)
456 # The only required arugment to popen is named "args", the rest are optional keyword arguments.
457 def popen(self, args, **kwargs):
458 # FIXME: We should always be stringifying the args, but callers who pass shell=True
459 # expect that the exact bytes passed will get passed to the shell (even if they're wrongly encoded).
460 # shell=True is wrong for many other reasons, and we should remove this
461 # hack as soon as we can fix all callers to not use shell=True.
462 if kwargs.get('shell') == True:
465 string_args = self._stringify_args(args)
466 return subprocess.Popen(string_args, **kwargs)
468 def call(self, args, **kwargs):
469 return subprocess.call(self._stringify_args(args), **kwargs)
471 def run_in_parallel(self, command_lines_and_cwds, processes=None):
472 """Runs a list of (cmd_line list, cwd string) tuples in parallel and returns a list of (retcode, stdout, stderr) tuples."""
473 assert len(command_lines_and_cwds)
475 if sys.platform in ('cygwin', 'win32'):
476 return map(_run_command_thunk, command_lines_and_cwds)
477 pool = multiprocessing.Pool(processes=processes)
478 results = pool.map(_run_command_thunk, command_lines_and_cwds)
484 def _run_command_thunk(cmd_line_and_cwd):
485 # Note that this needs to be a bare module (and hence Picklable) method to work with multiprocessing.Pool.
486 (cmd_line, cwd) = cmd_line_and_cwd
487 proc = subprocess.Popen(cmd_line, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
488 stdout, stderr = proc.communicate()
489 return (proc.returncode, stdout, stderr)