1a7e61ef26d159a42a35d7056d7bd878c23beee9
[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                 return child_process.poll()
112             # We assume that the child process wrote to us in utf-8,
113             # so no re-encoding is necessary before writing here.
114             teed_output.write(output_line)
115
116     # FIXME: Remove this deprecated method and move callers to run_command.
117     # FIXME: This method is a hack to allow running command which both
118     # capture their output and print out to stdin.  Useful for things
119     # like "build-webkit" where we want to display to the user that we're building
120     # but still have the output to stuff into a log file.
121     def run_and_throw_if_fail(self, args, quiet=False, decode_output=True):
122         # Cache the child's output locally so it can be used for error reports.
123         child_out_file = StringIO.StringIO()
124         tee_stdout = sys.stdout
125         if quiet:
126             dev_null = open(os.devnull, "w")  # FIXME: Does this need an encoding?
127             tee_stdout = dev_null
128         child_stdout = tee(child_out_file, tee_stdout)
129         exit_code = self._run_command_with_teed_output(args, child_stdout)
130         if quiet:
131             dev_null.close()
132
133         child_output = child_out_file.getvalue()
134         child_out_file.close()
135
136         # We assume the child process output utf-8
137         if decode_output:
138             child_output = child_output.decode("utf-8")
139
140         if exit_code:
141             raise ScriptError(script_args=args,
142                               exit_code=exit_code,
143                               output=child_output)
144         return child_output
145
146     def cpu_count(self):
147         if multiprocessing:
148             return multiprocessing.cpu_count()
149         # Darn.  We don't have the multiprocessing package.
150         system_name = platform.system()
151         if system_name == "Darwin":
152             return int(self.run_command(["sysctl", "-n", "hw.ncpu"]))
153         elif system_name == "Windows":
154             return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
155         elif system_name == "Linux":
156             num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
157             if isinstance(num_cores, int) and num_cores > 0:
158                 return num_cores
159         # This quantity is a lie but probably a reasonable guess for modern
160         # machines.
161         return 2
162
163     def kill_process(self, pid):
164         """Attempts to kill the given pid.
165         Will fail silently if pid does not exist or insufficient permisssions."""
166         if platform.system() == "Windows":
167             # According to http://docs.python.org/library/os.html
168             # os.kill isn't available on Windows.  However, when I tried it
169             # using Cygwin, it worked fine.  We should investigate whether
170             # we need this platform specific code here.
171             command = ["taskkill.exe", "/f", "/pid", str(pid)]
172             # taskkill will exit 128 if the process is not found.
173             self.run_command(command, error_handler=self.ignore_error)
174             return
175         try:
176             os.kill(pid, signal.SIGKILL)
177         except OSError, e:
178             # FIXME: We should make non-silent failure an option.
179             pass
180
181     def kill_all(self, process_name):
182         """Attempts to kill processes matching process_name.
183         Will fail silently if no process are found."""
184         if platform.system() == "Windows":
185             # We might want to automatically append .exe?
186             command = ["taskkill.exe", "/f", "/im", process_name]
187             # taskkill will exit 128 if the process is not found.
188             self.run_command(command, error_handler=self.ignore_error)
189             return
190
191         # FIXME: This is inconsistent that kill_all uses TERM and kill_process
192         # uses KILL.  Windows is always using /f (which seems like -KILL).
193         # We should pick one mode, or add support for switching between them.
194         # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
195         command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
196         self.run_command(command, error_handler=self.ignore_error)
197
198     # Error handlers do not need to be static methods once all callers are
199     # updated to use an Executive object.
200
201     @staticmethod
202     def default_error_handler(error):
203         raise error
204
205     @staticmethod
206     def ignore_error(error):
207         pass
208
209     def _compute_stdin(self, input):
210         """Returns (stdin, string_to_communicate)"""
211         # FIXME: We should be returning /dev/null for stdin
212         # or closing stdin after process creation to prevent
213         # child processes from getting input from the user.
214         if not input:
215             return (None, None)
216         if hasattr(input, "read"):  # Check if the input is a file.
217             return (input, None)  # Assume the file is in the right encoding.
218
219         # Popen in Python 2.5 and before does not automatically encode unicode objects.
220         # http://bugs.python.org/issue5290
221         # See https://bugs.webkit.org/show_bug.cgi?id=37528
222         # for an example of a regresion caused by passing a unicode string directly.
223         # FIXME: We may need to encode differently on different platforms.
224         if isinstance(input, unicode):
225             input = input.encode("utf-8")
226         return (subprocess.PIPE, input)
227
228     # FIXME: run_and_throw_if_fail should be merged into this method.
229     def run_command(self,
230                     args,
231                     cwd=None,
232                     input=None,
233                     error_handler=None,
234                     return_exit_code=False,
235                     return_stderr=True,
236                     decode_output=True):
237         """Popen wrapper for convenience and to work around python bugs."""
238         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
239         stdin, string_to_communicate = self._compute_stdin(input)
240         stderr = subprocess.STDOUT if return_stderr else None
241
242         process = subprocess.Popen(args,
243                                    stdin=stdin,
244                                    stdout=subprocess.PIPE,
245                                    stderr=stderr,
246                                    cwd=cwd,
247                                    close_fds=self._should_close_fds())
248         output = process.communicate(string_to_communicate)[0]
249         # run_command automatically decodes to unicode() unless explicitly told not to.
250         if decode_output:
251             output = output.decode("utf-8")
252         exit_code = process.wait()
253
254         if return_exit_code:
255             return exit_code
256
257         if exit_code:
258             script_error = ScriptError(script_args=args,
259                                        exit_code=exit_code,
260                                        output=output,
261                                        cwd=cwd)
262             (error_handler or self.default_error_handler)(script_error)
263         return output