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