11eb05165fcfc01dc59505545eb6735358171ce0
[WebKit-https.git] / WebKitTools / 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 os
37 import platform
38 import StringIO
39 import signal
40 import subprocess
41 import sys
42
43 from webkitpy.common.system.deprecated_logging import tee
44
45
46 class ScriptError(Exception):
47
48     def __init__(self,
49                  message=None,
50                  script_args=None,
51                  exit_code=None,
52                  output=None,
53                  cwd=None):
54         if not message:
55             message = 'Failed to run "%s"' % script_args
56             if exit_code:
57                 message += " exit_code: %d" % exit_code
58             if cwd:
59                 message += " cwd: %s" % cwd
60
61         Exception.__init__(self, message)
62         self.script_args = script_args # 'args' is already used by Exception
63         self.exit_code = exit_code
64         self.output = output
65         self.cwd = cwd
66
67     def message_with_output(self, output_limit=500):
68         if self.output:
69             if output_limit and len(self.output) > output_limit:
70                 return "%s\nLast %s characters of output:\n%s" % \
71                     (self, output_limit, self.output[-output_limit:])
72             return "%s\n%s" % (self, self.output)
73         return str(self)
74
75     def command_name(self):
76         command_path = self.script_args
77         if type(command_path) is list:
78             command_path = command_path[0]
79         return os.path.basename(command_path)
80
81
82 def run_command(*args, **kwargs):
83     # FIXME: This should not be a global static.
84     # New code should use Executive.run_command directly instead
85     return Executive().run_command(*args, **kwargs)
86
87
88 class Executive(object):
89
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')
97
98     def _run_command_with_teed_output(self, args, teed_output):
99         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
100         child_process = subprocess.Popen(args,
101                                          stdout=subprocess.PIPE,
102                                          stderr=subprocess.STDOUT,
103                                          close_fds=self._should_close_fds())
104
105         # Use our own custom wait loop because Popen ignores a tee'd
106         # stderr/stdout.
107         # FIXME: This could be improved not to flatten output to stdout.
108         while True:
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)
117
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):
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
127         if quiet:
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)
132         if quiet:
133             dev_null.close()
134
135         child_output = child_out_file.getvalue()
136         child_out_file.close()
137
138         # We assume the child process output utf-8
139         if decode_output:
140             child_output = child_output.decode("utf-8")
141
142         if exit_code:
143             raise ScriptError(script_args=args,
144                               exit_code=exit_code,
145                               output=child_output)
146         return child_output
147
148     def cpu_count(self):
149         if multiprocessing:
150             return multiprocessing.cpu_count()
151         # Darn.  We don't have the multiprocessing package.
152         system_name = platform.system()
153         if system_name == "Darwin":
154             return int(self.run_command(["sysctl", "-n", "hw.ncpu"]))
155         elif system_name == "Windows":
156             return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
157         elif system_name == "Linux":
158             num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
159             if isinstance(num_cores, int) and num_cores > 0:
160                 return num_cores
161         # This quantity is a lie but probably a reasonable guess for modern
162         # machines.
163         return 2
164
165     def kill_process(self, pid):
166         """Attempts to kill the given pid.
167         Will fail silently if pid does not exist or insufficient permisssions."""
168         if platform.system() == "Windows":
169             # According to http://docs.python.org/library/os.html
170             # os.kill isn't available on Windows.  However, when I tried it
171             # using Cygwin, it worked fine.  We should investigate whether
172             # we need this platform specific code here.
173             command = ["taskkill.exe", "/f", "/pid", str(pid)]
174             # taskkill will exit 128 if the process is not found.
175             self.run_command(command, error_handler=self.ignore_error)
176             return
177         try:
178             os.kill(pid, signal.SIGKILL)
179         except OSError, e:
180             # FIXME: We should make non-silent failure an option.
181             pass
182
183     def kill_all(self, process_name):
184         """Attempts to kill processes matching process_name.
185         Will fail silently if no process are found."""
186         if platform.system() == "Windows":
187             # We might want to automatically append .exe?
188             command = ["taskkill.exe", "/f", "/im", process_name]
189             # taskkill will exit 128 if the process is not found.
190             self.run_command(command, error_handler=self.ignore_error)
191             return
192
193         # FIXME: This is inconsistent that kill_all uses TERM and kill_process
194         # uses KILL.  Windows is always using /f (which seems like -KILL).
195         # We should pick one mode, or add support for switching between them.
196         # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
197         command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
198         self.run_command(command, error_handler=self.ignore_error)
199
200     # Error handlers do not need to be static methods once all callers are
201     # updated to use an Executive object.
202
203     @staticmethod
204     def default_error_handler(error):
205         raise error
206
207     @staticmethod
208     def ignore_error(error):
209         pass
210
211     def _compute_stdin(self, input):
212         """Returns (stdin, string_to_communicate)"""
213         # FIXME: We should be returning /dev/null for stdin
214         # or closing stdin after process creation to prevent
215         # child processes from getting input from the user.
216         if not input:
217             return (None, None)
218         if hasattr(input, "read"):  # Check if the input is a file.
219             return (input, None)  # Assume the file is in the right encoding.
220
221         # Popen in Python 2.5 and before does not automatically encode unicode objects.
222         # http://bugs.python.org/issue5290
223         # See https://bugs.webkit.org/show_bug.cgi?id=37528
224         # for an example of a regresion caused by passing a unicode string directly.
225         # FIXME: We may need to encode differently on different platforms.
226         if isinstance(input, unicode):
227             input = input.encode("utf-8")
228         return (subprocess.PIPE, input)
229
230     # FIXME: run_and_throw_if_fail should be merged into this method.
231     def run_command(self,
232                     args,
233                     cwd=None,
234                     input=None,
235                     error_handler=None,
236                     return_exit_code=False,
237                     return_stderr=True,
238                     decode_output=True):
239         """Popen wrapper for convenience and to work around python bugs."""
240         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
241         stdin, string_to_communicate = self._compute_stdin(input)
242         stderr = subprocess.STDOUT if return_stderr else None
243
244         process = subprocess.Popen(args,
245                                    stdin=stdin,
246                                    stdout=subprocess.PIPE,
247                                    stderr=stderr,
248                                    cwd=cwd,
249                                    close_fds=self._should_close_fds())
250         output = process.communicate(string_to_communicate)[0]
251         # run_command automatically decodes to unicode() unless explicitly told not to.
252         if decode_output:
253             output = output.decode("utf-8")
254         # wait() is not threadsafe and can throw OSError due to:
255         # http://bugs.python.org/issue1731717
256         exit_code = process.wait()
257
258         if return_exit_code:
259             return exit_code
260
261         if exit_code:
262             script_error = ScriptError(script_args=args,
263                                        exit_code=exit_code,
264                                        output=output,
265                                        cwd=cwd)
266             (error_handler or self.default_error_handler)(script_error)
267         return output