WebKitTools/Scripts/webkitpy/common/system/executive.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     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 errno
       
    37 import logging
       
    38 import os
       
    39 import platform
       
    40 import StringIO
       
    41 import signal
       
    42 import subprocess
       
    43 import sys
       
    44 import time
       
    45 
       
    46 from webkitpy.common.system.deprecated_logging import tee
       
    47 
       
    48 
       
    49 _log = logging.getLogger("webkitpy.common.system")
       
    50 
       
    51 
       
    52 class ScriptError(Exception):
       
    53 
       
    54     def __init__(self,
       
    55                  message=None,
       
    56                  script_args=None,
       
    57                  exit_code=None,
       
    58                  output=None,
       
    59                  cwd=None):
       
    60         if not message:
       
    61             message = 'Failed to run "%s"' % script_args
       
    62             if exit_code:
       
    63                 message += " exit_code: %d" % exit_code
       
    64             if cwd:
       
    65                 message += " cwd: %s" % cwd
       
    66 
       
    67         Exception.__init__(self, message)
       
    68         self.script_args = script_args # 'args' is already used by Exception
       
    69         self.exit_code = exit_code
       
    70         self.output = output
       
    71         self.cwd = cwd
       
    72 
       
    73     def message_with_output(self, output_limit=500):
       
    74         if self.output:
       
    75             if output_limit and len(self.output) > output_limit:
       
    76                 return "%s\nLast %s characters of output:\n%s" % \
       
    77                     (self, output_limit, self.output[-output_limit:])
       
    78             return "%s\n%s" % (self, self.output)
       
    79         return str(self)
       
    80 
       
    81     def command_name(self):
       
    82         command_path = self.script_args
       
    83         if type(command_path) is list:
       
    84             command_path = command_path[0]
       
    85         return os.path.basename(command_path)
       
    86 
       
    87 
       
    88 def run_command(*args, **kwargs):
       
    89     # FIXME: This should not be a global static.
       
    90     # New code should use Executive.run_command directly instead
       
    91     return Executive().run_command(*args, **kwargs)
       
    92 
       
    93 
       
    94 class Executive(object):
       
    95 
       
    96     def _should_close_fds(self):
       
    97         # We need to pass close_fds=True to work around Python bug #2320
       
    98         # (otherwise we can hang when we kill DumpRenderTree when we are running
       
    99         # multiple threads). See http://bugs.python.org/issue2320 .
       
   100         # Note that close_fds isn't supported on Windows, but this bug only
       
   101         # shows up on Mac and Linux.
       
   102         return sys.platform not in ('win32', 'cygwin')
       
   103 
       
   104     def _run_command_with_teed_output(self, args, teed_output):
       
   105         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
       
   106         child_process = subprocess.Popen(args,
       
   107                                          stdout=subprocess.PIPE,
       
   108                                          stderr=subprocess.STDOUT,
       
   109                                          close_fds=self._should_close_fds())
       
   110 
       
   111         # Use our own custom wait loop because Popen ignores a tee'd
       
   112         # stderr/stdout.
       
   113         # FIXME: This could be improved not to flatten output to stdout.
       
   114         while True:
       
   115             output_line = child_process.stdout.readline()
       
   116             if output_line == "" and child_process.poll() != None:
       
   117                 # poll() is not threadsafe and can throw OSError due to:
       
   118                 # http://bugs.python.org/issue1731717
       
   119                 return child_process.poll()
       
   120             # We assume that the child process wrote to us in utf-8,
       
   121             # so no re-encoding is necessary before writing here.
       
   122             teed_output.write(output_line)
       
   123 
       
   124     # FIXME: Remove this deprecated method and move callers to run_command.
       
   125     # FIXME: This method is a hack to allow running command which both
       
   126     # capture their output and print out to stdin.  Useful for things
       
   127     # like "build-webkit" where we want to display to the user that we're building
       
   128     # but still have the output to stuff into a log file.
       
   129     def run_and_throw_if_fail(self, args, quiet=False, decode_output=True):
       
   130         # Cache the child's output locally so it can be used for error reports.
       
   131         child_out_file = StringIO.StringIO()
       
   132         tee_stdout = sys.stdout
       
   133         if quiet:
       
   134             dev_null = open(os.devnull, "w")  # FIXME: Does this need an encoding?
       
   135             tee_stdout = dev_null
       
   136         child_stdout = tee(child_out_file, tee_stdout)
       
   137         exit_code = self._run_command_with_teed_output(args, child_stdout)
       
   138         if quiet:
       
   139             dev_null.close()
       
   140 
       
   141         child_output = child_out_file.getvalue()
       
   142         child_out_file.close()
       
   143 
       
   144         # We assume the child process output utf-8
       
   145         if decode_output:
       
   146             child_output = child_output.decode("utf-8")
       
   147 
       
   148         if exit_code:
       
   149             raise ScriptError(script_args=args,
       
   150                               exit_code=exit_code,
       
   151                               output=child_output)
       
   152         return child_output
       
   153 
       
   154     def cpu_count(self):
       
   155         if multiprocessing:
       
   156             return multiprocessing.cpu_count()
       
   157         # Darn.  We don't have the multiprocessing package.
       
   158         system_name = platform.system()
       
   159         if system_name == "Darwin":
       
   160             return int(self.run_command(["sysctl", "-n", "hw.ncpu"]))
       
   161         elif system_name == "Windows":
       
   162             return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
       
   163         elif system_name == "Linux":
       
   164             num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
       
   165             if isinstance(num_cores, int) and num_cores > 0:
       
   166                 return num_cores
       
   167         # This quantity is a lie but probably a reasonable guess for modern
       
   168         # machines.
       
   169         return 2
       
   170 
       
   171     def kill_process(self, pid):
       
   172         """Attempts to kill the given pid.
       
   173         Will fail silently if pid does not exist or insufficient permisssions."""
       
   174         if sys.platform == "win32":
       
   175             # We only use taskkill.exe on windows (not cygwin) because subprocess.pid
       
   176             # is a CYGWIN pid and taskkill.exe expects a windows pid.
       
   177             # Thankfully os.kill on CYGWIN handles either pid type.
       
   178             command = ["taskkill.exe", "/f", "/pid", pid]
       
   179             # taskkill will exit 128 if the process is not found.  We should log.
       
   180             self.run_command(command, error_handler=self.ignore_error)
       
   181             return
       
   182 
       
   183         # According to http://docs.python.org/library/os.html
       
   184         # os.kill isn't available on Windows. python 2.5.5 os.kill appears
       
   185         # to work in cygwin, however it occasionally raises EAGAIN.
       
   186         retries_left = 10 if sys.platform == "cygwin" else 1
       
   187         while retries_left > 0:
       
   188             try:
       
   189                 retries_left -= 1
       
   190                 os.kill(pid, signal.SIGKILL)
       
   191             except OSError, e:
       
   192                 if e.errno == errno.EAGAIN:
       
   193                     if retries_left <= 0:
       
   194                         _log.warn("Failed to kill pid %s.  Too many EAGAIN errors." % pid)
       
   195                     continue
       
   196                 if e.errno == errno.ESRCH:  # The process does not exist.
       
   197                     _log.warn("Called kill_process with a non-existant pid %s" % pid)
       
   198                     return
       
   199                 raise
       
   200 
       
   201     def _windows_image_name(self, process_name):
       
   202         name, extension = os.path.splitext(process_name)
       
   203         if not extension:
       
   204             # taskkill expects processes to end in .exe
       
   205             # If necessary we could add a flag to disable appending .exe.
       
   206             process_name = "%s.exe" % name
       
   207         return process_name
       
   208 
       
   209     def kill_all(self, process_name):
       
   210         """Attempts to kill processes matching process_name.
       
   211         Will fail silently if no process are found."""
       
   212         if sys.platform in ("win32", "cygwin"):
       
   213             image_name = self._windows_image_name(process_name)
       
   214             command = ["taskkill.exe", "/f", "/im", image_name]
       
   215             # taskkill will exit 128 if the process is not found.  We should log.
       
   216             self.run_command(command, error_handler=self.ignore_error)
       
   217             return
       
   218 
       
   219         # FIXME: This is inconsistent that kill_all uses TERM and kill_process
       
   220         # uses KILL.  Windows is always using /f (which seems like -KILL).
       
   221         # We should pick one mode, or add support for switching between them.
       
   222         # Note: Mac OS X 10.6 requires -SIGNALNAME before -u USER
       
   223         command = ["killall", "-TERM", "-u", os.getenv("USER"), process_name]
       
   224         # killall returns 1 if no process can be found and 2 on command error.
       
   225         # FIXME: We should pass a custom error_handler to allow only exit_code 1.
       
   226         # We should log in exit_code == 1
       
   227         self.run_command(command, error_handler=self.ignore_error)
       
   228 
       
   229     # Error handlers do not need to be static methods once all callers are
       
   230     # updated to use an Executive object.
       
   231 
       
   232     @staticmethod
       
   233     def default_error_handler(error):
       
   234         raise error
       
   235 
       
   236     @staticmethod
       
   237     def ignore_error(error):
       
   238         pass
       
   239 
       
   240     def _compute_stdin(self, input):
       
   241         """Returns (stdin, string_to_communicate)"""
       
   242         # FIXME: We should be returning /dev/null for stdin
       
   243         # or closing stdin after process creation to prevent
       
   244         # child processes from getting input from the user.
       
   245         if not input:
       
   246             return (None, None)
       
   247         if hasattr(input, "read"):  # Check if the input is a file.
       
   248             return (input, None)  # Assume the file is in the right encoding.
       
   249 
       
   250         # Popen in Python 2.5 and before does not automatically encode unicode objects.
       
   251         # http://bugs.python.org/issue5290
       
   252         # See https://bugs.webkit.org/show_bug.cgi?id=37528
       
   253         # for an example of a regresion caused by passing a unicode string directly.
       
   254         # FIXME: We may need to encode differently on different platforms.
       
   255         if isinstance(input, unicode):
       
   256             input = input.encode("utf-8")
       
   257         return (subprocess.PIPE, input)
       
   258 
       
   259     def _command_for_printing(self, args):
       
   260         """Returns a print-ready string representing command args.
       
   261         The string should be copy/paste ready for execution in a shell."""
       
   262         escaped_args = []
       
   263         for arg in args:
       
   264             if isinstance(arg, unicode):
       
   265                 # Escape any non-ascii characters for easy copy/paste
       
   266                 arg = arg.encode("unicode_escape")
       
   267             # FIXME: Do we need to fix quotes here?
       
   268             escaped_args.append(arg)
       
   269         return " ".join(escaped_args)
       
   270 
       
   271     # FIXME: run_and_throw_if_fail should be merged into this method.
       
   272     def run_command(self,
       
   273                     args,
       
   274                     cwd=None,
       
   275                     input=None,
       
   276                     error_handler=None,
       
   277                     return_exit_code=False,
       
   278                     return_stderr=True,
       
   279                     decode_output=True):
       
   280         """Popen wrapper for convenience and to work around python bugs."""
       
   281         assert(isinstance(args, list) or isinstance(args, tuple))
       
   282         start_time = time.time()
       
   283         args = map(unicode, args)  # Popen will throw an exception if args are non-strings (like int())
       
   284         stdin, string_to_communicate = self._compute_stdin(input)
       
   285         stderr = subprocess.STDOUT if return_stderr else None
       
   286 
       
   287         process = subprocess.Popen(args,
       
   288                                    stdin=stdin,
       
   289                                    stdout=subprocess.PIPE,
       
   290                                    stderr=stderr,
       
   291                                    cwd=cwd,
       
   292                                    close_fds=self._should_close_fds())
       
   293         output = process.communicate(string_to_communicate)[0]
       
   294         # run_command automatically decodes to unicode() unless explicitly told not to.
       
   295         if decode_output:
       
   296             output = output.decode("utf-8")
       
   297         # wait() is not threadsafe and can throw OSError due to:
       
   298         # http://bugs.python.org/issue1731717
       
   299         exit_code = process.wait()
       
   300 
       
   301         _log.debug('"%s" took %.2fs' % (self._command_for_printing(args), time.time() - start_time))
       
   302 
       
   303         if return_exit_code:
       
   304             return exit_code
       
   305 
       
   306         if exit_code:
       
   307             script_error = ScriptError(script_args=args,
       
   308                                        exit_code=exit_code,
       
   309                                        output=output,
       
   310                                        cwd=cwd)
       
   311             (error_handler or self.default_error_handler)(script_error)
       
   312         return output