procutil.py
546 lines
| 18.1 KiB
| text/x-python
|
PythonLexer
Yuya Nishihara
|
r37136 | # procutil.py - utility for managing processes and executable environment | ||
# | ||||
# Copyright 2005 K. Thananchayan <thananck@yahoo.com> | ||||
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
Yuya Nishihara
|
r37142 | import contextlib | ||
Augie Fackler
|
r40532 | import errno | ||
Yuya Nishihara
|
r37136 | import imp | ||
import io | ||||
import os | ||||
import signal | ||||
import subprocess | ||||
import sys | ||||
import time | ||||
from ..i18n import _ | ||||
from .. import ( | ||||
encoding, | ||||
error, | ||||
policy, | ||||
pycompat, | ||||
) | ||||
osutil = policy.importmod(r'osutil') | ||||
stderr = pycompat.stderr | ||||
stdin = pycompat.stdin | ||||
stdout = pycompat.stdout | ||||
def isatty(fp): | ||||
try: | ||||
return fp.isatty() | ||||
except AttributeError: | ||||
return False | ||||
# glibc determines buffering on first write to stdout - if we replace a TTY | ||||
# destined stdout with a pipe destined stdout (e.g. pager), we want line | ||||
Sune Foldager
|
r38485 | # buffering (or unbuffered, on Windows) | ||
Yuya Nishihara
|
r37136 | if isatty(stdout): | ||
Sune Foldager
|
r38485 | if pycompat.iswindows: | ||
# Windows doesn't support line buffering | ||||
stdout = os.fdopen(stdout.fileno(), r'wb', 0) | ||||
else: | ||||
stdout = os.fdopen(stdout.fileno(), r'wb', 1) | ||||
Yuya Nishihara
|
r37136 | |||
if pycompat.iswindows: | ||||
from .. import windows as platform | ||||
stdout = platform.winstdout(stdout) | ||||
else: | ||||
from .. import posix as platform | ||||
findexe = platform.findexe | ||||
_gethgcmd = platform.gethgcmd | ||||
getuser = platform.getuser | ||||
getpid = os.getpid | ||||
hidewindow = platform.hidewindow | ||||
quotecommand = platform.quotecommand | ||||
readpipe = platform.readpipe | ||||
setbinary = platform.setbinary | ||||
setsignalhandler = platform.setsignalhandler | ||||
shellquote = platform.shellquote | ||||
shellsplit = platform.shellsplit | ||||
spawndetached = platform.spawndetached | ||||
sshargs = platform.sshargs | ||||
testpid = platform.testpid | ||||
try: | ||||
setprocname = osutil.setprocname | ||||
except AttributeError: | ||||
pass | ||||
try: | ||||
unblocksignal = osutil.unblocksignal | ||||
except AttributeError: | ||||
pass | ||||
closefds = pycompat.isposix | ||||
Yuya Nishihara
|
r37478 | def explainexit(code): | ||
Yuya Nishihara
|
r37481 | """return a message describing a subprocess status | ||
Yuya Nishihara
|
r37478 | (codes from kill are negative - not os.system/wait encoding)""" | ||
if code >= 0: | ||||
Yuya Nishihara
|
r37481 | return _("exited with status %d") % code | ||
return _("killed by signal %d") % -code | ||||
Yuya Nishihara
|
r37478 | |||
Yuya Nishihara
|
r37477 | class _pfile(object): | ||
"""File-like wrapper for a stream opened by subprocess.Popen()""" | ||||
def __init__(self, proc, fp): | ||||
self._proc = proc | ||||
self._fp = fp | ||||
def close(self): | ||||
# unlike os.popen(), this returns an integer in subprocess coding | ||||
self._fp.close() | ||||
return self._proc.wait() | ||||
def __iter__(self): | ||||
return iter(self._fp) | ||||
def __getattr__(self, attr): | ||||
return getattr(self._fp, attr) | ||||
def __enter__(self): | ||||
return self | ||||
def __exit__(self, exc_type, exc_value, exc_tb): | ||||
self.close() | ||||
def popen(cmd, mode='rb', bufsize=-1): | ||||
if mode == 'rb': | ||||
return _popenreader(cmd, bufsize) | ||||
elif mode == 'wb': | ||||
return _popenwriter(cmd, bufsize) | ||||
raise error.ProgrammingError('unsupported mode: %r' % mode) | ||||
def _popenreader(cmd, bufsize): | ||||
Matt Harbison
|
r39851 | p = subprocess.Popen(tonativestr(quotecommand(cmd)), | ||
shell=True, bufsize=bufsize, | ||||
Yuya Nishihara
|
r37477 | close_fds=closefds, | ||
stdout=subprocess.PIPE) | ||||
return _pfile(p, p.stdout) | ||||
def _popenwriter(cmd, bufsize): | ||||
Matt Harbison
|
r39851 | p = subprocess.Popen(tonativestr(quotecommand(cmd)), | ||
shell=True, bufsize=bufsize, | ||||
Yuya Nishihara
|
r37477 | close_fds=closefds, | ||
stdin=subprocess.PIPE) | ||||
return _pfile(p, p.stdin) | ||||
Yuya Nishihara
|
r37482 | def popen2(cmd, env=None): | ||
Yuya Nishihara
|
r37136 | # Setting bufsize to -1 lets the system decide the buffer size. | ||
# The default for bufsize is 0, meaning unbuffered. This leads to | ||||
# poor performance on Mac OS X: http://bugs.python.org/issue4194 | ||||
Matt Harbison
|
r39868 | p = subprocess.Popen(tonativestr(cmd), | ||
Matt Harbison
|
r39851 | shell=True, bufsize=-1, | ||
Yuya Nishihara
|
r37136 | close_fds=closefds, | ||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, | ||||
Matt Harbison
|
r39851 | env=tonativeenv(env)) | ||
Yuya Nishihara
|
r37136 | return p.stdin, p.stdout | ||
Yuya Nishihara
|
r37482 | def popen3(cmd, env=None): | ||
stdin, stdout, stderr, p = popen4(cmd, env) | ||||
Yuya Nishihara
|
r37136 | return stdin, stdout, stderr | ||
Yuya Nishihara
|
r37482 | def popen4(cmd, env=None, bufsize=-1): | ||
Matt Harbison
|
r39868 | p = subprocess.Popen(tonativestr(cmd), | ||
Matt Harbison
|
r39851 | shell=True, bufsize=bufsize, | ||
Yuya Nishihara
|
r37136 | close_fds=closefds, | ||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, | ||||
stderr=subprocess.PIPE, | ||||
Matt Harbison
|
r39851 | env=tonativeenv(env)) | ||
Yuya Nishihara
|
r37136 | return p.stdin, p.stdout, p.stderr, p | ||
def pipefilter(s, cmd): | ||||
'''filter string S through command CMD, returning its output''' | ||||
Matt Harbison
|
r39868 | p = subprocess.Popen(tonativestr(cmd), | ||
Matt Harbison
|
r39851 | shell=True, close_fds=closefds, | ||
Yuya Nishihara
|
r37136 | stdin=subprocess.PIPE, stdout=subprocess.PIPE) | ||
pout, perr = p.communicate(s) | ||||
return pout | ||||
def tempfilter(s, cmd): | ||||
'''filter string S through a pair of temporary files with CMD. | ||||
CMD is used as a template to create the real command to be run, | ||||
with the strings INFILE and OUTFILE replaced by the real names of | ||||
the temporary files generated.''' | ||||
inname, outname = None, None | ||||
try: | ||||
Yuya Nishihara
|
r38182 | infd, inname = pycompat.mkstemp(prefix='hg-filter-in-') | ||
Yuya Nishihara
|
r37136 | fp = os.fdopen(infd, r'wb') | ||
fp.write(s) | ||||
fp.close() | ||||
Yuya Nishihara
|
r38182 | outfd, outname = pycompat.mkstemp(prefix='hg-filter-out-') | ||
Yuya Nishihara
|
r37136 | os.close(outfd) | ||
cmd = cmd.replace('INFILE', inname) | ||||
cmd = cmd.replace('OUTFILE', outname) | ||||
Yuya Nishihara
|
r37479 | code = system(cmd) | ||
Yuya Nishihara
|
r37136 | if pycompat.sysplatform == 'OpenVMS' and code & 1: | ||
code = 0 | ||||
if code: | ||||
raise error.Abort(_("command '%s' failed: %s") % | ||||
Yuya Nishihara
|
r37481 | (cmd, explainexit(code))) | ||
Yuya Nishihara
|
r37136 | with open(outname, 'rb') as fp: | ||
return fp.read() | ||||
finally: | ||||
try: | ||||
if inname: | ||||
os.unlink(inname) | ||||
except OSError: | ||||
pass | ||||
try: | ||||
if outname: | ||||
os.unlink(outname) | ||||
except OSError: | ||||
pass | ||||
_filtertable = { | ||||
'tempfile:': tempfilter, | ||||
'pipe:': pipefilter, | ||||
} | ||||
def filter(s, cmd): | ||||
"filter a string through a command that transforms its input to its output" | ||||
for name, fn in _filtertable.iteritems(): | ||||
if cmd.startswith(name): | ||||
return fn(s, cmd[len(name):].lstrip()) | ||||
return pipefilter(s, cmd) | ||||
def mainfrozen(): | ||||
"""return True if we are a frozen executable. | ||||
The code supports py2exe (most common, Windows only) and tools/freeze | ||||
(portable, not much used). | ||||
""" | ||||
return (pycompat.safehasattr(sys, "frozen") or # new py2exe | ||||
pycompat.safehasattr(sys, "importers") or # old py2exe | ||||
imp.is_frozen(u"__main__")) # tools/freeze | ||||
_hgexecutable = None | ||||
def hgexecutable(): | ||||
"""return location of the 'hg' executable. | ||||
Defaults to $HG or 'hg' in the search path. | ||||
""" | ||||
if _hgexecutable is None: | ||||
hg = encoding.environ.get('HG') | ||||
mainmod = sys.modules[r'__main__'] | ||||
if hg: | ||||
_sethgexecutable(hg) | ||||
elif mainfrozen(): | ||||
if getattr(sys, 'frozen', None) == 'macosx_app': | ||||
# Env variable set by py2app | ||||
_sethgexecutable(encoding.environ['EXECUTABLEPATH']) | ||||
else: | ||||
_sethgexecutable(pycompat.sysexecutable) | ||||
elif (os.path.basename( | ||||
pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'): | ||||
_sethgexecutable(pycompat.fsencode(mainmod.__file__)) | ||||
else: | ||||
exe = findexe('hg') or os.path.basename(sys.argv[0]) | ||||
_sethgexecutable(exe) | ||||
return _hgexecutable | ||||
def _sethgexecutable(path): | ||||
"""set location of the 'hg' executable""" | ||||
global _hgexecutable | ||||
_hgexecutable = path | ||||
def _testfileno(f, stdf): | ||||
fileno = getattr(f, 'fileno', None) | ||||
try: | ||||
return fileno and fileno() == stdf.fileno() | ||||
except io.UnsupportedOperation: | ||||
return False # fileno() raised UnsupportedOperation | ||||
def isstdin(f): | ||||
return _testfileno(f, sys.__stdin__) | ||||
def isstdout(f): | ||||
return _testfileno(f, sys.__stdout__) | ||||
Yuya Nishihara
|
r37141 | def protectstdio(uin, uout): | ||
Yuya Nishihara
|
r37237 | """Duplicate streams and redirect original if (uin, uout) are stdio | ||
If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's | ||||
redirected to stderr so the output is still readable. | ||||
Yuya Nishihara
|
r37141 | |||
Returns (fin, fout) which point to the original (uin, uout) fds, but | ||||
may be copy of (uin, uout). The returned streams can be considered | ||||
"owned" in that print(), exec(), etc. never reach to them. | ||||
""" | ||||
uout.flush() | ||||
Yuya Nishihara
|
r37236 | fin, fout = uin, uout | ||
Yuya Nishihara
|
r39873 | if _testfileno(uin, stdin): | ||
Yuya Nishihara
|
r37236 | newfd = os.dup(uin.fileno()) | ||
Yuya Nishihara
|
r37237 | nullfd = os.open(os.devnull, os.O_RDONLY) | ||
Yuya Nishihara
|
r37236 | os.dup2(nullfd, uin.fileno()) | ||
Yuya Nishihara
|
r37237 | os.close(nullfd) | ||
Yuya Nishihara
|
r37236 | fin = os.fdopen(newfd, r'rb') | ||
Yuya Nishihara
|
r39873 | if _testfileno(uout, stdout): | ||
Yuya Nishihara
|
r37236 | newfd = os.dup(uout.fileno()) | ||
Yuya Nishihara
|
r37237 | os.dup2(stderr.fileno(), uout.fileno()) | ||
Yuya Nishihara
|
r37236 | fout = os.fdopen(newfd, r'wb') | ||
return fin, fout | ||||
Yuya Nishihara
|
r37141 | |||
def restorestdio(uin, uout, fin, fout): | ||||
"""Restore (uin, uout) streams from possibly duplicated (fin, fout)""" | ||||
uout.flush() | ||||
for f, uif in [(fin, uin), (fout, uout)]: | ||||
if f is not uif: | ||||
os.dup2(f.fileno(), uif.fileno()) | ||||
f.close() | ||||
Yuya Nishihara
|
r37142 | @contextlib.contextmanager | ||
def protectedstdio(uin, uout): | ||||
"""Run code block with protected standard streams""" | ||||
fin, fout = protectstdio(uin, uout) | ||||
try: | ||||
yield fin, fout | ||||
finally: | ||||
restorestdio(uin, uout, fin, fout) | ||||
Yuya Nishihara
|
r37136 | def shellenviron(environ=None): | ||
"""return environ with optional override, useful for shelling out""" | ||||
def py2shell(val): | ||||
'convert python object into string that is useful to shell' | ||||
if val is None or val is False: | ||||
return '0' | ||||
if val is True: | ||||
return '1' | ||||
return pycompat.bytestr(val) | ||||
env = dict(encoding.environ) | ||||
if environ: | ||||
env.update((k, py2shell(v)) for k, v in environ.iteritems()) | ||||
env['HG'] = hgexecutable() | ||||
return env | ||||
Matt Harbison
|
r38510 | if pycompat.iswindows: | ||
def shelltonative(cmd, env): | ||||
return platform.shelltocmdexe(cmd, shellenviron(env)) | ||||
Matt Harbison
|
r39698 | |||
tonativestr = encoding.strfromlocal | ||||
Matt Harbison
|
r38510 | else: | ||
def shelltonative(cmd, env): | ||||
return cmd | ||||
Matt Harbison
|
r39698 | tonativestr = pycompat.identity | ||
def tonativeenv(env): | ||||
'''convert the environment from bytes to strings suitable for Popen(), etc. | ||||
''' | ||||
return pycompat.rapply(tonativestr, env) | ||||
Yuya Nishihara
|
r37136 | def system(cmd, environ=None, cwd=None, out=None): | ||
'''enhanced shell command execution. | ||||
run with environment maybe modified, maybe in different dir. | ||||
if out is specified, it is assumed to be a file-like object that has a | ||||
write() method. stdout and stderr will be redirected to out.''' | ||||
try: | ||||
stdout.flush() | ||||
except Exception: | ||||
pass | ||||
cmd = quotecommand(cmd) | ||||
env = shellenviron(environ) | ||||
if out is None or isstdout(out): | ||||
Matt Harbison
|
r39868 | rc = subprocess.call(tonativestr(cmd), | ||
Matt Harbison
|
r39851 | shell=True, close_fds=closefds, | ||
env=tonativeenv(env), | ||||
cwd=pycompat.rapply(tonativestr, cwd)) | ||||
Yuya Nishihara
|
r37136 | else: | ||
Matt Harbison
|
r39868 | proc = subprocess.Popen(tonativestr(cmd), | ||
Matt Harbison
|
r39851 | shell=True, close_fds=closefds, | ||
env=tonativeenv(env), | ||||
cwd=pycompat.rapply(tonativestr, cwd), | ||||
stdout=subprocess.PIPE, | ||||
Yuya Nishihara
|
r37136 | stderr=subprocess.STDOUT) | ||
for line in iter(proc.stdout.readline, ''): | ||||
out.write(line) | ||||
proc.wait() | ||||
rc = proc.returncode | ||||
if pycompat.sysplatform == 'OpenVMS' and rc & 1: | ||||
rc = 0 | ||||
return rc | ||||
def gui(): | ||||
'''Are we running in a GUI?''' | ||||
if pycompat.isdarwin: | ||||
if 'SSH_CONNECTION' in encoding.environ: | ||||
# handle SSH access to a box where the user is logged in | ||||
return False | ||||
elif getattr(osutil, 'isgui', None): | ||||
# check if a CoreGraphics session is available | ||||
return osutil.isgui() | ||||
else: | ||||
# pure build; use a safe default | ||||
return True | ||||
else: | ||||
return pycompat.iswindows or encoding.environ.get("DISPLAY") | ||||
def hgcmd(): | ||||
"""Return the command used to execute current hg | ||||
This is different from hgexecutable() because on Windows we want | ||||
to avoid things opening new shell windows like batch files, so we | ||||
get either the python call or current executable. | ||||
""" | ||||
if mainfrozen(): | ||||
if getattr(sys, 'frozen', None) == 'macosx_app': | ||||
# Env variable set by py2app | ||||
return [encoding.environ['EXECUTABLEPATH']] | ||||
else: | ||||
return [pycompat.sysexecutable] | ||||
return _gethgcmd() | ||||
def rundetached(args, condfn): | ||||
"""Execute the argument list in a detached process. | ||||
condfn is a callable which is called repeatedly and should return | ||||
True once the child process is known to have started successfully. | ||||
At this point, the child process PID is returned. If the child | ||||
process fails to start or finishes before condfn() evaluates to | ||||
True, return -1. | ||||
""" | ||||
# Windows case is easier because the child process is either | ||||
# successfully starting and validating the condition or exiting | ||||
# on failure. We just poll on its PID. On Unix, if the child | ||||
# process fails to start, it will be left in a zombie state until | ||||
# the parent wait on it, which we cannot do since we expect a long | ||||
# running process on success. Instead we listen for SIGCHLD telling | ||||
# us our child process terminated. | ||||
terminated = set() | ||||
def handler(signum, frame): | ||||
terminated.add(os.wait()) | ||||
prevhandler = None | ||||
SIGCHLD = getattr(signal, 'SIGCHLD', None) | ||||
if SIGCHLD is not None: | ||||
prevhandler = signal.signal(SIGCHLD, handler) | ||||
try: | ||||
pid = spawndetached(args) | ||||
while not condfn(): | ||||
if ((pid in terminated or not testpid(pid)) | ||||
and not condfn()): | ||||
return -1 | ||||
time.sleep(0.1) | ||||
return pid | ||||
finally: | ||||
if prevhandler is not None: | ||||
signal.signal(signal.SIGCHLD, prevhandler) | ||||
Augie Fackler
|
r38545 | |||
@contextlib.contextmanager | ||||
def uninterruptable(warn): | ||||
"""Inhibit SIGINT handling on a region of code. | ||||
Note that if this is called in a non-main thread, it turns into a no-op. | ||||
Args: | ||||
warn: A callable which takes no arguments, and returns True if the | ||||
previous signal handling should be restored. | ||||
""" | ||||
oldsiginthandler = [signal.getsignal(signal.SIGINT)] | ||||
shouldbail = [] | ||||
def disabledsiginthandler(*args): | ||||
if warn(): | ||||
signal.signal(signal.SIGINT, oldsiginthandler[0]) | ||||
del oldsiginthandler[0] | ||||
shouldbail.append(True) | ||||
try: | ||||
try: | ||||
signal.signal(signal.SIGINT, disabledsiginthandler) | ||||
except ValueError: | ||||
# wrong thread, oh well, we tried | ||||
del oldsiginthandler[0] | ||||
yield | ||||
finally: | ||||
if oldsiginthandler: | ||||
signal.signal(signal.SIGINT, oldsiginthandler[0]) | ||||
if shouldbail: | ||||
raise KeyboardInterrupt | ||||
Augie Fackler
|
r40532 | |||
if pycompat.iswindows: | ||||
# no fork on Windows, but we can create a detached process | ||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx | ||||
# No stdlib constant exists for this value | ||||
DETACHED_PROCESS = 0x00000008 | ||||
Boris Feld
|
r40572 | # Following creation flags might create a console GUI window. | ||
# Using subprocess.CREATE_NEW_CONSOLE might helps. | ||||
# See https://phab.mercurial-scm.org/D1701 for discussion | ||||
Augie Fackler
|
r40532 | _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP | ||
def runbgcommand(script, env, shell=False, stdout=None, stderr=None): | ||||
'''Spawn a command without waiting for it to finish.''' | ||||
# we can't use close_fds *and* redirect stdin. I'm not sure that we | ||||
# need to because the detached process has no console connection. | ||||
subprocess.Popen( | ||||
Augie Fackler
|
r40533 | tonativestr(script), | ||
shell=shell, env=tonativeenv(env), close_fds=True, | ||||
creationflags=_creationflags, stdout=stdout, | ||||
stderr=stderr) | ||||
Augie Fackler
|
r40532 | else: | ||
def runbgcommand(cmd, env, shell=False, stdout=None, stderr=None): | ||||
'''Spawn a command without waiting for it to finish.''' | ||||
# double-fork to completely detach from the parent process | ||||
# based on http://code.activestate.com/recipes/278731 | ||||
pid = os.fork() | ||||
if pid: | ||||
# Parent process | ||||
(_pid, status) = os.waitpid(pid, 0) | ||||
if os.WIFEXITED(status): | ||||
returncode = os.WEXITSTATUS(status) | ||||
else: | ||||
returncode = -os.WTERMSIG(status) | ||||
if returncode != 0: | ||||
# The child process's return code is 0 on success, an errno | ||||
# value on failure, or 255 if we don't have a valid errno | ||||
# value. | ||||
# | ||||
# (It would be slightly nicer to return the full exception info | ||||
# over a pipe as the subprocess module does. For now it | ||||
# doesn't seem worth adding that complexity here, though.) | ||||
if returncode == 255: | ||||
returncode = errno.EINVAL | ||||
raise OSError(returncode, 'error running %r: %s' % | ||||
(cmd, os.strerror(returncode))) | ||||
return | ||||
returncode = 255 | ||||
try: | ||||
# Start a new session | ||||
os.setsid() | ||||
stdin = open(os.devnull, 'r') | ||||
if stdout is None: | ||||
stdout = open(os.devnull, 'w') | ||||
if stderr is None: | ||||
stderr = open(os.devnull, 'w') | ||||
# connect stdin to devnull to make sure the subprocess can't | ||||
# muck up that stream for mercurial. | ||||
subprocess.Popen( | ||||
cmd, shell=shell, env=env, close_fds=True, | ||||
stdin=stdin, stdout=stdout, stderr=stderr) | ||||
returncode = 0 | ||||
except EnvironmentError as ex: | ||||
returncode = (ex.errno & 0xff) | ||||
if returncode == 0: | ||||
# This shouldn't happen, but just in case make sure the | ||||
# return code is never 0 here. | ||||
returncode = 255 | ||||
except Exception: | ||||
returncode = 255 | ||||
finally: | ||||
# mission accomplished, this child needs to exit and not | ||||
# continue the hg process here. | ||||
os._exit(returncode) | ||||