mercurial/utils/procutil.py
changeset 37118 5be286db5fb5
parent 37117 e7b517809ebc
child 37123 0216232f21ab
equal deleted inserted replaced
37117:e7b517809ebc 37118:5be286db5fb5
       
     1 # procutil.py - utility for managing processes and executable environment
       
     2 #
       
     3 #  Copyright 2005 K. Thananchayan <thananck@yahoo.com>
       
     4 #  Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
       
     5 #  Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
       
     6 #
       
     7 # This software may be used and distributed according to the terms of the
       
     8 # GNU General Public License version 2 or any later version.
       
     9 
       
    10 from __future__ import absolute_import
       
    11 
       
    12 import imp
       
    13 import io
       
    14 import os
       
    15 import signal
       
    16 import subprocess
       
    17 import sys
       
    18 import tempfile
       
    19 import time
       
    20 
       
    21 from ..i18n import _
       
    22 
       
    23 from .. import (
       
    24     encoding,
       
    25     error,
       
    26     policy,
       
    27     pycompat,
       
    28 )
       
    29 
       
    30 osutil = policy.importmod(r'osutil')
       
    31 
       
    32 stderr = pycompat.stderr
       
    33 stdin = pycompat.stdin
       
    34 stdout = pycompat.stdout
       
    35 
       
    36 def isatty(fp):
       
    37     try:
       
    38         return fp.isatty()
       
    39     except AttributeError:
       
    40         return False
       
    41 
       
    42 # glibc determines buffering on first write to stdout - if we replace a TTY
       
    43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
       
    44 # buffering
       
    45 if isatty(stdout):
       
    46     stdout = os.fdopen(stdout.fileno(), r'wb', 1)
       
    47 
       
    48 if pycompat.iswindows:
       
    49     from .. import windows as platform
       
    50     stdout = platform.winstdout(stdout)
       
    51 else:
       
    52     from .. import posix as platform
       
    53 
       
    54 explainexit = platform.explainexit
       
    55 findexe = platform.findexe
       
    56 _gethgcmd = platform.gethgcmd
       
    57 getuser = platform.getuser
       
    58 getpid = os.getpid
       
    59 hidewindow = platform.hidewindow
       
    60 popen = platform.popen
       
    61 quotecommand = platform.quotecommand
       
    62 readpipe = platform.readpipe
       
    63 setbinary = platform.setbinary
       
    64 setsignalhandler = platform.setsignalhandler
       
    65 shellquote = platform.shellquote
       
    66 shellsplit = platform.shellsplit
       
    67 spawndetached = platform.spawndetached
       
    68 sshargs = platform.sshargs
       
    69 testpid = platform.testpid
       
    70 
       
    71 try:
       
    72     setprocname = osutil.setprocname
       
    73 except AttributeError:
       
    74     pass
       
    75 try:
       
    76     unblocksignal = osutil.unblocksignal
       
    77 except AttributeError:
       
    78     pass
       
    79 
       
    80 closefds = pycompat.isposix
       
    81 
       
    82 def popen2(cmd, env=None, newlines=False):
       
    83     # Setting bufsize to -1 lets the system decide the buffer size.
       
    84     # The default for bufsize is 0, meaning unbuffered. This leads to
       
    85     # poor performance on Mac OS X: http://bugs.python.org/issue4194
       
    86     p = subprocess.Popen(cmd, shell=True, bufsize=-1,
       
    87                          close_fds=closefds,
       
    88                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
       
    89                          universal_newlines=newlines,
       
    90                          env=env)
       
    91     return p.stdin, p.stdout
       
    92 
       
    93 def popen3(cmd, env=None, newlines=False):
       
    94     stdin, stdout, stderr, p = popen4(cmd, env, newlines)
       
    95     return stdin, stdout, stderr
       
    96 
       
    97 def popen4(cmd, env=None, newlines=False, bufsize=-1):
       
    98     p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
       
    99                          close_fds=closefds,
       
   100                          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
       
   101                          stderr=subprocess.PIPE,
       
   102                          universal_newlines=newlines,
       
   103                          env=env)
       
   104     return p.stdin, p.stdout, p.stderr, p
       
   105 
       
   106 def pipefilter(s, cmd):
       
   107     '''filter string S through command CMD, returning its output'''
       
   108     p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
       
   109                          stdin=subprocess.PIPE, stdout=subprocess.PIPE)
       
   110     pout, perr = p.communicate(s)
       
   111     return pout
       
   112 
       
   113 def tempfilter(s, cmd):
       
   114     '''filter string S through a pair of temporary files with CMD.
       
   115     CMD is used as a template to create the real command to be run,
       
   116     with the strings INFILE and OUTFILE replaced by the real names of
       
   117     the temporary files generated.'''
       
   118     inname, outname = None, None
       
   119     try:
       
   120         infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
       
   121         fp = os.fdopen(infd, r'wb')
       
   122         fp.write(s)
       
   123         fp.close()
       
   124         outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
       
   125         os.close(outfd)
       
   126         cmd = cmd.replace('INFILE', inname)
       
   127         cmd = cmd.replace('OUTFILE', outname)
       
   128         code = os.system(cmd)
       
   129         if pycompat.sysplatform == 'OpenVMS' and code & 1:
       
   130             code = 0
       
   131         if code:
       
   132             raise error.Abort(_("command '%s' failed: %s") %
       
   133                               (cmd, explainexit(code)))
       
   134         with open(outname, 'rb') as fp:
       
   135             return fp.read()
       
   136     finally:
       
   137         try:
       
   138             if inname:
       
   139                 os.unlink(inname)
       
   140         except OSError:
       
   141             pass
       
   142         try:
       
   143             if outname:
       
   144                 os.unlink(outname)
       
   145         except OSError:
       
   146             pass
       
   147 
       
   148 _filtertable = {
       
   149     'tempfile:': tempfilter,
       
   150     'pipe:': pipefilter,
       
   151 }
       
   152 
       
   153 def filter(s, cmd):
       
   154     "filter a string through a command that transforms its input to its output"
       
   155     for name, fn in _filtertable.iteritems():
       
   156         if cmd.startswith(name):
       
   157             return fn(s, cmd[len(name):].lstrip())
       
   158     return pipefilter(s, cmd)
       
   159 
       
   160 def mainfrozen():
       
   161     """return True if we are a frozen executable.
       
   162 
       
   163     The code supports py2exe (most common, Windows only) and tools/freeze
       
   164     (portable, not much used).
       
   165     """
       
   166     return (pycompat.safehasattr(sys, "frozen") or # new py2exe
       
   167             pycompat.safehasattr(sys, "importers") or # old py2exe
       
   168             imp.is_frozen(u"__main__")) # tools/freeze
       
   169 
       
   170 _hgexecutable = None
       
   171 
       
   172 def hgexecutable():
       
   173     """return location of the 'hg' executable.
       
   174 
       
   175     Defaults to $HG or 'hg' in the search path.
       
   176     """
       
   177     if _hgexecutable is None:
       
   178         hg = encoding.environ.get('HG')
       
   179         mainmod = sys.modules[r'__main__']
       
   180         if hg:
       
   181             _sethgexecutable(hg)
       
   182         elif mainfrozen():
       
   183             if getattr(sys, 'frozen', None) == 'macosx_app':
       
   184                 # Env variable set by py2app
       
   185                 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
       
   186             else:
       
   187                 _sethgexecutable(pycompat.sysexecutable)
       
   188         elif (os.path.basename(
       
   189             pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
       
   190             _sethgexecutable(pycompat.fsencode(mainmod.__file__))
       
   191         else:
       
   192             exe = findexe('hg') or os.path.basename(sys.argv[0])
       
   193             _sethgexecutable(exe)
       
   194     return _hgexecutable
       
   195 
       
   196 def _sethgexecutable(path):
       
   197     """set location of the 'hg' executable"""
       
   198     global _hgexecutable
       
   199     _hgexecutable = path
       
   200 
       
   201 def _testfileno(f, stdf):
       
   202     fileno = getattr(f, 'fileno', None)
       
   203     try:
       
   204         return fileno and fileno() == stdf.fileno()
       
   205     except io.UnsupportedOperation:
       
   206         return False # fileno() raised UnsupportedOperation
       
   207 
       
   208 def isstdin(f):
       
   209     return _testfileno(f, sys.__stdin__)
       
   210 
       
   211 def isstdout(f):
       
   212     return _testfileno(f, sys.__stdout__)
       
   213 
       
   214 def shellenviron(environ=None):
       
   215     """return environ with optional override, useful for shelling out"""
       
   216     def py2shell(val):
       
   217         'convert python object into string that is useful to shell'
       
   218         if val is None or val is False:
       
   219             return '0'
       
   220         if val is True:
       
   221             return '1'
       
   222         return pycompat.bytestr(val)
       
   223     env = dict(encoding.environ)
       
   224     if environ:
       
   225         env.update((k, py2shell(v)) for k, v in environ.iteritems())
       
   226     env['HG'] = hgexecutable()
       
   227     return env
       
   228 
       
   229 def system(cmd, environ=None, cwd=None, out=None):
       
   230     '''enhanced shell command execution.
       
   231     run with environment maybe modified, maybe in different dir.
       
   232 
       
   233     if out is specified, it is assumed to be a file-like object that has a
       
   234     write() method. stdout and stderr will be redirected to out.'''
       
   235     try:
       
   236         stdout.flush()
       
   237     except Exception:
       
   238         pass
       
   239     cmd = quotecommand(cmd)
       
   240     env = shellenviron(environ)
       
   241     if out is None or isstdout(out):
       
   242         rc = subprocess.call(cmd, shell=True, close_fds=closefds,
       
   243                              env=env, cwd=cwd)
       
   244     else:
       
   245         proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
       
   246                                 env=env, cwd=cwd, stdout=subprocess.PIPE,
       
   247                                 stderr=subprocess.STDOUT)
       
   248         for line in iter(proc.stdout.readline, ''):
       
   249             out.write(line)
       
   250         proc.wait()
       
   251         rc = proc.returncode
       
   252     if pycompat.sysplatform == 'OpenVMS' and rc & 1:
       
   253         rc = 0
       
   254     return rc
       
   255 
       
   256 def gui():
       
   257     '''Are we running in a GUI?'''
       
   258     if pycompat.isdarwin:
       
   259         if 'SSH_CONNECTION' in encoding.environ:
       
   260             # handle SSH access to a box where the user is logged in
       
   261             return False
       
   262         elif getattr(osutil, 'isgui', None):
       
   263             # check if a CoreGraphics session is available
       
   264             return osutil.isgui()
       
   265         else:
       
   266             # pure build; use a safe default
       
   267             return True
       
   268     else:
       
   269         return pycompat.iswindows or encoding.environ.get("DISPLAY")
       
   270 
       
   271 def hgcmd():
       
   272     """Return the command used to execute current hg
       
   273 
       
   274     This is different from hgexecutable() because on Windows we want
       
   275     to avoid things opening new shell windows like batch files, so we
       
   276     get either the python call or current executable.
       
   277     """
       
   278     if mainfrozen():
       
   279         if getattr(sys, 'frozen', None) == 'macosx_app':
       
   280             # Env variable set by py2app
       
   281             return [encoding.environ['EXECUTABLEPATH']]
       
   282         else:
       
   283             return [pycompat.sysexecutable]
       
   284     return _gethgcmd()
       
   285 
       
   286 def rundetached(args, condfn):
       
   287     """Execute the argument list in a detached process.
       
   288 
       
   289     condfn is a callable which is called repeatedly and should return
       
   290     True once the child process is known to have started successfully.
       
   291     At this point, the child process PID is returned. If the child
       
   292     process fails to start or finishes before condfn() evaluates to
       
   293     True, return -1.
       
   294     """
       
   295     # Windows case is easier because the child process is either
       
   296     # successfully starting and validating the condition or exiting
       
   297     # on failure. We just poll on its PID. On Unix, if the child
       
   298     # process fails to start, it will be left in a zombie state until
       
   299     # the parent wait on it, which we cannot do since we expect a long
       
   300     # running process on success. Instead we listen for SIGCHLD telling
       
   301     # us our child process terminated.
       
   302     terminated = set()
       
   303     def handler(signum, frame):
       
   304         terminated.add(os.wait())
       
   305     prevhandler = None
       
   306     SIGCHLD = getattr(signal, 'SIGCHLD', None)
       
   307     if SIGCHLD is not None:
       
   308         prevhandler = signal.signal(SIGCHLD, handler)
       
   309     try:
       
   310         pid = spawndetached(args)
       
   311         while not condfn():
       
   312             if ((pid in terminated or not testpid(pid))
       
   313                 and not condfn()):
       
   314                 return -1
       
   315             time.sleep(0.1)
       
   316         return pid
       
   317     finally:
       
   318         if prevhandler is not None:
       
   319             signal.signal(signal.SIGCHLD, prevhandler)