9155350e806bcae25909bcec79a590afe0b17fdd
[WebKit.git] / Tools / Scripts / webkitpy / common / system / executive.py
1 # Copyright (c) 2009, Google Inc. All rights reserved.
2 # Copyright (c) 2009 Apple Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
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
13 # distribution.
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.
17 #
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.
29
30 try:
31     # This API exists only in Python 2.6 and higher.  :(
32     import multiprocessing
33 except ImportError:
34     multiprocessing = None
35
36 import ctypes
37 import errno
38 import logging
39 import os
40 import platform
41 import StringIO
42 import signal
43 import subprocess
44 import sys
45 import time
46
47 from webkitpy.common.system.deprecated_logging import tee
48 from webkitpy.python24 import versioning
49
50
51 _log = logging.getLogger("webkitpy.common.system")
52
53
54 class ScriptError(Exception):
55
56     # This is a custom List.__str__ implementation to allow size limiting.
57     def _string_from_args(self, args, limit=100):
58         args_string = unicode(args)
59         # We could make this much fancier, but for now this is OK.
60         if len(args_string) > limit:
61             return args_string[:limit - 3] + "..."
62         return args_string
63
64     def __init__(self,
65                  message=None,
66                  script_args=None,
67                  exit_code=None,
68                  output=None,
69                  cwd=None):
70         if not message:
71             message = 'Failed to run "%s"' % self._string_from_args(script_args)
72             if exit_code:
73                 message += " exit_code: %d" % exit_code
74             if cwd:
75                 message += " cwd: %s" % cwd
76
77         Exception.__init__(self, message)
78         self.script_args = script_args # 'args' is already used by Exception
79         self.exit_code = exit_code
80         self.output = output
81         self.cwd = cwd
82
83     def message_with_output(self, output_limit=500):
84         if self.output:
85             if output_limit and len(self.output) > output_limit:
86                 return u"%s\n\nLast %s characters of output:\n%s" % \
87                     (self, output_limit, self.output[-output_limit:])
88             return u"%s\n\n%s" % (self, self.output)
89         return unicode(self)
90
91     def command_name(self):
92         command_path = self.script_args
93         if type(command_path) is list:
94             command_path = command_path[0]
95         return os.path.basename(command_path)
96
97
98 def run_command(*args, **kwargs):
99     # FIXME: This should not be a global static.
100     # New code should use Executive.run_command directly instead
101     return Executive().run_command(*args, **kwargs)
102
103
104 class Executive(object):
105
106     def _should_close_fds(self):
107         # We need to pass close_fds=True to work around Python bug #2320
108         # (otherwise we can hang when we kill DumpRenderTree when we are running
109         # multiple threads). See http://bugs.python.org/issue2320 .
110         # Note that close_fds isn't supported on Windows, but this bug only
111         # shows up on Mac and Linux.
112         return sys.platform not in ('win32', 'cygwin')
113
114     def _should_use_shell(self):
115         # On Windows, if we don't use the shell, we don't search %PATH% to
116         # find the command-- an absolute path is required.
117         return sys.platform.startswith('win')
118
119     def _run_command_with_teed_output(self, args, teed_output):
120         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
121         args = map(self._encode_argument_if_needed, args)
122
123         child_process = subprocess.Popen(args,
124                                          stdout=subprocess.PIPE,
125                                          stderr=subprocess.STDOUT,
126                                          close_fds=self._should_close_fds(),
127                                          shell=self._should_use_shell())
128
129         # Use our own custom wait loop because Popen ignores a tee'd
130         # stderr/stdout.
131         # FIXME: This could be improved not to flatten output to stdout.
132         while True:
133             output_line = child_process.stdout.readline()
134             if output_line == "" and child_process.poll() != None:
135                 # poll() is not threadsafe and can throw OSError due to:
136                 # http://bugs.python.org/issue1731717
137                 return child_process.poll()
138             # We assume that the child process wrote to us in utf-8,
139             # so no re-encoding is necessary before writing here.
140             teed_output.write(output_line)
141
142     # FIXME: Remove this deprecated method and move callers to run_command.
143     # FIXME: This method is a hack to allow running command which both
144     # capture their output and print out to stdin.  Useful for things
145     # like "build-webkit" where we want to display to the user that we're building
146     # but still have the output to stuff into a log file.
147     def run_and_throw_if_fail(self, args, quiet=False, decode_output=True):
148         # Cache the child's output locally so it can be used for error reports.
149         child_out_file = StringIO.StringIO()
150         tee_stdout = sys.stdout
151         if quiet:
152             dev_null = open(os.devnull, "w")  # FIXME: Does this need an encoding?
153             tee_stdout = dev_null
154         child_stdout = tee(child_out_file, tee_stdout)
155         exit_code = self._run_command_with_teed_output(args, child_stdout)
156         if quiet:
157             dev_null.close()
158
159         child_output = child_out_file.getvalue()
160         child_out_file.close()
161
162         if decode_output:
163             child_output = child_output.decode(self._child_process_encoding())
164
165         if exit_code:
166             raise ScriptError(script_args=args,
167                               exit_code=exit_code,
168                               output=child_output)
169         return child_output
170
171     def cpu_count(self):
172         if multiprocessing:
173             return multiprocessing.cpu_count()
174         # Darn.  We don't have the multiprocessing package.
175         system_name = platform.system()
176         if system_name == "Darwin":
177             return int(self.run_command(["sysctl", "-n", "hw.ncpu"]))
178         elif system_name == "Windows":
179             return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
180         elif system_name == "Linux":
181             num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
182             if isinstance(num_cores, int) and num_cores > 0:
183                 return num_cores
184         # This quantity is a lie but probably a reasonable guess for modern
185         # machines.
186         return 2
187
188     def kill_process(self, pid):
189         """Attempts to kill the given pid.
190         Will fail silently if pid does not exist or insufficient permisssions."""
191         if sys.platform == "win32":
192             # We only use taskkill.exe on windows (not cygwin) because subprocess.pid
193             # is a CYGWIN pid and taskkill.exe expects a windows pid.
194             # Thankfully os.kill on CYGWIN handles either pid type.
195             command = ["taskkill.exe", "/f", "/pid", pid]
196             # taskkill will exit 128 if the process is not found.  We should log.
197             self.run_command(command, error_handler=self.ignore_error)
198             return
199
200         # According to http://docs.python.org/library/os.html
201         # os.kill isn't available on Windows. python 2.5.5 os.kill appears
202         # to work in cygwin, however it occasionally raises EAGAIN.
203         retries_left = 10 if sys.platform == "cygwin" else 1
204         while retries_left > 0:
205             try:
206                 retries_left -= 1
207                 os.kill(pid, signal.SIGKILL)
208             except OSError, e:
209                 if e.errno == errno.EAGAIN:
210                     if retries_left <= 0:
211                         _log.warn("Failed to kill pid %s.  Too many EAGAIN errors." % pid)
212                     continue
213                 if e.errno == errno.ESRCH:  # The process does not exist.
214                     _log.warn("Called kill_process with a non-existant pid %s" % pid)
215                     return
216                 raise
217
218     def _win32_check_running_pid(self, pid):
219
220         class PROCESSENTRY32(ctypes.Structure):
221             _fields_ = [("dwSize", ctypes.c_ulong),
222                         ("cntUsage", ctypes.c_ulong),
223                         ("th32ProcessID", ctypes.c_ulong),
224                         ("th32DefaultHeapID", ctypes.c_ulong),
225                         ("th32ModuleID", ctypes.c_ulong),
226                         ("cntThreads", ctypes.c_ulong),
227                         ("th32ParentProcessID", ctypes.c_ulong),
228                         ("pcPriClassBase", ctypes.c_ulong),
229                         ("dwFlags", ctypes.c_ulong),
230                         ("szExeFile", ctypes.c_char * 260)]
231
232         CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
233         Process32First = ctypes.windll.kernel32.Process32First
234         Process32Next = ctypes.windll.kernel32.Process32Next
235         CloseHandle = ctypes.windll.kernel32.CloseHandle
236         TH32CS_SNAPPROCESS = 0x00000002  # win32 magic number
237         hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
238         pe32 = PROCESSENTRY32()
239         pe32.dwSize = ctypes.sizeof(PROCESSENTRY32)
240         result = False
241         if not Process32First(hProcessSnap, ctypes.byref(pe32)):
242             _log.debug("Failed getting first process.")
243             CloseHandle(hProcessSnap)
244             return result
245         while True:
246             if pe32.th32ProcessID == pid:
247                 result = True
248                 break
249             if not Process32Next(hProcessSnap, ctypes.byref(pe32)):
250                 break
251         CloseHandle(hProcessSnap)
252         return result
253
254     def check_running_pid(self, pid):
255         """Return True if pid is alive, otherwise return False."""
256         if sys.platform in ('darwin', 'linux2', 'cygwin'):
257             try:
258                 os.kill(pid, 0)
259                 return True
260             except OSError:
261                 return False
262         elif sys.platform == 'win32':
263             return self._win32_check_running_pid(pid)
264
265         assert(False)
266
267     def _windows_image_name(self, process_name):
268         name, extension = os.path.splitext(process_name)
269         if not extension:
270             # taskkill expects processes to end in .exe
271             # If necessary we could add a flag to disable appending .exe.
272             process_name = "%s.exe" % name
273         return process_name
274
275     def kill_all(self, process_name):
276         """Attempts to kill processes matching process_name.
277         Will fail silently if no process are found."""
278         if sys.platform in ("win32", "cygwin"):
279             image_name = self._windows_image_name(process_name)
280             command = ["taskkill.exe", "/f", "/im", image_name]
281             # taskkill will exit 128 if the process is not found.  We should log.
282             self.run_command(command, error_handler=self.ignore_error)
283             return
284
285         # FIXME: This is inconsistent that kill_all uses TERM and kill_process
286         # uses KILL.  Windows is always using /f (which seems like -KILL).
287         # We should pick one mode, or add support for switching between them.
288         # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
289         command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
290         # killall returns 1 if no process can be found and 2 on command error.
291         # FIXME: We should pass a custom error_handler to allow only exit_code 1.
292         # We should log in exit_code == 1
293         self.run_command(command, error_handler=self.ignore_error)
294
295     # Error handlers do not need to be static methods once all callers are
296     # updated to use an Executive object.
297
298     @staticmethod
299     def default_error_handler(error):
300         raise error
301
302     @staticmethod
303     def ignore_error(error):
304         pass
305
306     def _compute_stdin(self, input):
307         """Returns (stdin, string_to_communicate)"""
308         # FIXME: We should be returning /dev/null for stdin
309         # or closing stdin after process creation to prevent
310         # child processes from getting input from the user.
311         if not input:
312             return (None, None)
313         if hasattr(input, "read"):  # Check if the input is a file.
314             return (input, None)  # Assume the file is in the right encoding.
315
316         # Popen in Python 2.5 and before does not automatically encode unicode objects.
317         # http://bugs.python.org/issue5290
318         # See https://bugs.webkit.org/show_bug.cgi?id=37528
319         # for an example of a regresion caused by passing a unicode string directly.
320         # FIXME: We may need to encode differently on different platforms.
321         if isinstance(input, unicode):
322             input = input.encode(self._child_process_encoding())
323         return (subprocess.PIPE, input)
324
325     def _command_for_printing(self, args):
326         """Returns a print-ready string representing command args.
327         The string should be copy/paste ready for execution in a shell."""
328         escaped_args = []
329         for arg in args:
330             if isinstance(arg, unicode):
331                 # Escape any non-ascii characters for easy copy/paste
332                 arg = arg.encode("unicode_escape")
333             # FIXME: Do we need to fix quotes here?
334             escaped_args.append(arg)
335         return " ".join(escaped_args)
336
337     # FIXME: run_and_throw_if_fail should be merged into this method.
338     def run_command(self,
339                     args,
340                     cwd=None,
341                     input=None,
342                     error_handler=None,
343                     return_exit_code=False,
344                     return_stderr=True,
345                     decode_output=True):
346         """Popen wrapper for convenience and to work around python bugs."""
347         assert(isinstance(args, list) or isinstance(args, tuple))
348         start_time = time.time()
349         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
350         args = map(self._encode_argument_if_needed, args)
351
352         stdin, string_to_communicate = self._compute_stdin(input)
353         stderr = subprocess.STDOUT if return_stderr else None
354
355         process = subprocess.Popen(args,
356                                    stdin=stdin,
357                                    stdout=subprocess.PIPE,
358                                    stderr=stderr,
359                                    cwd=cwd,
360                                    close_fds=self._should_close_fds(),
361                                    shell=self._should_use_shell())
362         output = process.communicate(string_to_communicate)[0]
363
364         # run_command automatically decodes to unicode() unless explicitly told not to.
365         if decode_output:
366             output = output.decode(self._child_process_encoding())
367
368         # wait() is not threadsafe and can throw OSError due to:
369         # http://bugs.python.org/issue1731717
370         exit_code = process.wait()
371
372         _log.debug('"%s" took %.2fs' % (self._command_for_printing(args), time.time() - start_time))
373
374         if return_exit_code:
375             return exit_code
376
377         if exit_code:
378             script_error = ScriptError(script_args=args,
379                                        exit_code=exit_code,
380                                        output=output,
381                                        cwd=cwd)
382             (error_handler or self.default_error_handler)(script_error)
383         return output
384
385     def _child_process_encoding(self):
386         # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
387         # to launch subprocesses, so we have to encode arguments using the
388         # current code page.
389         if sys.platform == 'win32' and versioning.compare_version(sys, '3.0')[0] < 0:
390             return 'mbcs'
391         # All other platforms use UTF-8.
392         # FIXME: Using UTF-8 on Cygwin will confuse Windows-native commands
393         # which will expect arguments to be encoded using the current code
394         # page.
395         return 'utf-8'
396
397     def _should_encode_child_process_arguments(self):
398         # Cygwin's Python's os.execv doesn't support unicode command
399         # arguments, and neither does Cygwin's execv itself.
400         if sys.platform == 'cygwin':
401             return True
402
403         # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW
404         # to launch subprocesses, so we have to encode arguments using the
405         # current code page.
406         if sys.platform == 'win32' and versioning.compare_version(sys, '3.0')[0] < 0:
407             return True
408
409         return False
410
411     def _encode_argument_if_needed(self, argument):
412         if not self._should_encode_child_process_arguments():
413             return argument
414         return argument.encode(self._child_process_encoding())