procutil.py
701 lines
| 20.3 KiB
| text/x-python
|
PythonLexer
|
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 | ||||
|
r37142 | import contextlib | ||
|
r40532 | import errno | ||
|
r37136 | import io | ||
import os | ||||
import signal | ||||
import subprocess | ||||
import sys | ||||
|
r45304 | import threading | ||
|
r37136 | import time | ||
from ..i18n import _ | ||||
|
r43359 | from ..pycompat import ( | ||
getattr, | ||||
open, | ||||
) | ||||
|
r37136 | |||
from .. import ( | ||||
encoding, | ||||
error, | ||||
policy, | ||||
pycompat, | ||||
) | ||||
|
r44067 | # Import like this to keep import-checker happy | ||
from ..utils import resourceutil | ||||
|
r43906 | osutil = policy.importmod('osutil') | ||
|
r37136 | |||
stderr = pycompat.stderr | ||||
stdin = pycompat.stdin | ||||
stdout = pycompat.stdout | ||||
|
r43346 | |||
|
r37136 | def isatty(fp): | ||
try: | ||||
return fp.isatty() | ||||
except AttributeError: | ||||
return False | ||||
|
r43346 | |||
|
r45477 | if pycompat.ispy3: | ||
class LineBufferedWrapper(object): | ||||
def __init__(self, orig): | ||||
self.orig = orig | ||||
def __getattr__(self, attr): | ||||
return getattr(self.orig, attr) | ||||
def write(self, s): | ||||
orig = self.orig | ||||
res = orig.write(s) | ||||
if s.endswith(b'\n'): | ||||
orig.flush() | ||||
return res | ||||
io.BufferedIOBase.register(LineBufferedWrapper) | ||||
|
r37136 | # 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 | ||||
|
r38485 | # buffering (or unbuffered, on Windows) | ||
|
r37136 | if isatty(stdout): | ||
|
r38485 | if pycompat.iswindows: | ||
# Windows doesn't support line buffering | ||||
|
r43906 | stdout = os.fdopen(stdout.fileno(), 'wb', 0) | ||
|
r45477 | elif pycompat.ispy3: | ||
# On Python 3, buffered binary streams can't be set line-buffered. | ||||
# Therefore we have a wrapper that implements line buffering. | ||||
if isinstance(stdout, io.BufferedIOBase) and not isinstance( | ||||
stdout, LineBufferedWrapper | ||||
): | ||||
stdout = LineBufferedWrapper(stdout) | ||||
else: | ||||
|
r43906 | stdout = os.fdopen(stdout.fileno(), 'wb', 1) | ||
|
r37136 | |||
if pycompat.iswindows: | ||||
from .. import windows as platform | ||||
|
r43346 | |||
|
r37136 | 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 | ||||
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 | ||||
|
r43346 | |||
|
r37478 | def explainexit(code): | ||
|
r37481 | """return a message describing a subprocess status | ||
|
r37478 | (codes from kill are negative - not os.system/wait encoding)""" | ||
if code >= 0: | ||||
|
r43347 | return _(b"exited with status %d") % code | ||
return _(b"killed by signal %d") % -code | ||||
|
r37478 | |||
|
r43346 | |||
|
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() | ||||
|
r43346 | |||
|
r43347 | def popen(cmd, mode=b'rb', bufsize=-1): | ||
if mode == b'rb': | ||||
|
r37477 | return _popenreader(cmd, bufsize) | ||
|
r43347 | elif mode == b'wb': | ||
|
r37477 | return _popenwriter(cmd, bufsize) | ||
|
r43347 | raise error.ProgrammingError(b'unsupported mode: %r' % mode) | ||
|
r37477 | |||
|
r43346 | |||
|
r37477 | def _popenreader(cmd, bufsize): | ||
|
r43346 | p = subprocess.Popen( | ||
|
r45403 | tonativestr(cmd), | ||
|
r43346 | shell=True, | ||
bufsize=bufsize, | ||||
close_fds=closefds, | ||||
stdout=subprocess.PIPE, | ||||
) | ||||
|
r37477 | return _pfile(p, p.stdout) | ||
|
r43346 | |||
|
r37477 | def _popenwriter(cmd, bufsize): | ||
|
r43346 | p = subprocess.Popen( | ||
|
r45403 | tonativestr(cmd), | ||
|
r43346 | shell=True, | ||
bufsize=bufsize, | ||||
close_fds=closefds, | ||||
stdin=subprocess.PIPE, | ||||
) | ||||
|
r37477 | return _pfile(p, p.stdin) | ||
|
r43346 | |||
|
r37482 | def popen2(cmd, env=None): | ||
|
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 | ||||
|
r43346 | p = subprocess.Popen( | ||
tonativestr(cmd), | ||||
shell=True, | ||||
bufsize=-1, | ||||
close_fds=closefds, | ||||
stdin=subprocess.PIPE, | ||||
stdout=subprocess.PIPE, | ||||
env=tonativeenv(env), | ||||
) | ||||
|
r37136 | return p.stdin, p.stdout | ||
|
r43346 | |||
|
r37482 | def popen3(cmd, env=None): | ||
stdin, stdout, stderr, p = popen4(cmd, env) | ||||
|
r37136 | return stdin, stdout, stderr | ||
|
r43346 | |||
|
r37482 | def popen4(cmd, env=None, bufsize=-1): | ||
|
r43346 | p = subprocess.Popen( | ||
tonativestr(cmd), | ||||
shell=True, | ||||
bufsize=bufsize, | ||||
close_fds=closefds, | ||||
stdin=subprocess.PIPE, | ||||
stdout=subprocess.PIPE, | ||||
stderr=subprocess.PIPE, | ||||
env=tonativeenv(env), | ||||
) | ||||
|
r37136 | return p.stdin, p.stdout, p.stderr, p | ||
|
r43346 | |||
|
r37136 | def pipefilter(s, cmd): | ||
'''filter string S through command CMD, returning its output''' | ||||
|
r43346 | p = subprocess.Popen( | ||
tonativestr(cmd), | ||||
shell=True, | ||||
close_fds=closefds, | ||||
stdin=subprocess.PIPE, | ||||
stdout=subprocess.PIPE, | ||||
) | ||||
|
r37136 | pout, perr = p.communicate(s) | ||
return pout | ||||
|
r43346 | |||
|
r37136 | 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: | ||||
|
r43347 | infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-') | ||
|
r43906 | fp = os.fdopen(infd, 'wb') | ||
|
r37136 | fp.write(s) | ||
fp.close() | ||||
|
r43347 | outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-') | ||
|
r37136 | os.close(outfd) | ||
|
r43347 | cmd = cmd.replace(b'INFILE', inname) | ||
cmd = cmd.replace(b'OUTFILE', outname) | ||||
|
r37479 | code = system(cmd) | ||
|
r43347 | if pycompat.sysplatform == b'OpenVMS' and code & 1: | ||
|
r37136 | code = 0 | ||
if code: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"command '%s' failed: %s") % (cmd, explainexit(code)) | ||
|
r43346 | ) | ||
|
r43347 | with open(outname, b'rb') as fp: | ||
|
r37136 | return fp.read() | ||
finally: | ||||
try: | ||||
if inname: | ||||
os.unlink(inname) | ||||
except OSError: | ||||
pass | ||||
try: | ||||
if outname: | ||||
os.unlink(outname) | ||||
except OSError: | ||||
pass | ||||
|
r43346 | |||
|
r37136 | _filtertable = { | ||
|
r43347 | b'tempfile:': tempfilter, | ||
b'pipe:': pipefilter, | ||||
|
r37136 | } | ||
|
r43346 | |||
|
r37136 | def filter(s, cmd): | ||
|
r44226 | """filter a string through a command that transforms its input to its | ||
output""" | ||||
|
r43376 | for name, fn in pycompat.iteritems(_filtertable): | ||
|
r37136 | if cmd.startswith(name): | ||
|
r43346 | return fn(s, cmd[len(name) :].lstrip()) | ||
|
r37136 | return pipefilter(s, cmd) | ||
|
r43346 | |||
|
r37136 | _hgexecutable = None | ||
|
r43346 | |||
|
r37136 | def hgexecutable(): | ||
"""return location of the 'hg' executable. | ||||
Defaults to $HG or 'hg' in the search path. | ||||
""" | ||||
if _hgexecutable is None: | ||||
|
r43347 | hg = encoding.environ.get(b'HG') | ||
|
r43906 | mainmod = sys.modules['__main__'] | ||
|
r37136 | if hg: | ||
_sethgexecutable(hg) | ||||
|
r44067 | elif resourceutil.mainfrozen(): | ||
|
r44056 | if getattr(sys, 'frozen', None) == 'macosx_app': | ||
|
r37136 | # Env variable set by py2app | ||
|
r43347 | _sethgexecutable(encoding.environ[b'EXECUTABLEPATH']) | ||
|
r37136 | else: | ||
_sethgexecutable(pycompat.sysexecutable) | ||||
|
r43346 | elif ( | ||
not pycompat.iswindows | ||||
|
r44055 | and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg' | ||
|
r43346 | ): | ||
|
r37136 | _sethgexecutable(pycompat.fsencode(mainmod.__file__)) | ||
else: | ||||
|
r43346 | _sethgexecutable( | ||
|
r43347 | findexe(b'hg') or os.path.basename(pycompat.sysargv[0]) | ||
|
r43346 | ) | ||
|
r37136 | return _hgexecutable | ||
|
r43346 | |||
|
r37136 | def _sethgexecutable(path): | ||
"""set location of the 'hg' executable""" | ||||
global _hgexecutable | ||||
_hgexecutable = path | ||||
|
r43346 | |||
|
r37136 | def _testfileno(f, stdf): | ||
fileno = getattr(f, 'fileno', None) | ||||
try: | ||||
return fileno and fileno() == stdf.fileno() | ||||
except io.UnsupportedOperation: | ||||
|
r43346 | return False # fileno() raised UnsupportedOperation | ||
|
r37136 | |||
def isstdin(f): | ||||
return _testfileno(f, sys.__stdin__) | ||||
|
r43346 | |||
|
r37136 | def isstdout(f): | ||
return _testfileno(f, sys.__stdout__) | ||||
|
r43346 | |||
|
r37141 | def protectstdio(uin, uout): | ||
|
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. | ||||
|
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() | ||||
|
r37236 | fin, fout = uin, uout | ||
|
r39873 | if _testfileno(uin, stdin): | ||
|
r37236 | newfd = os.dup(uin.fileno()) | ||
|
r37237 | nullfd = os.open(os.devnull, os.O_RDONLY) | ||
|
r37236 | os.dup2(nullfd, uin.fileno()) | ||
|
r37237 | os.close(nullfd) | ||
|
r43906 | fin = os.fdopen(newfd, 'rb') | ||
|
r39873 | if _testfileno(uout, stdout): | ||
|
r37236 | newfd = os.dup(uout.fileno()) | ||
|
r37237 | os.dup2(stderr.fileno(), uout.fileno()) | ||
|
r43906 | fout = os.fdopen(newfd, 'wb') | ||
|
r37236 | return fin, fout | ||
|
r37141 | |||
|
r43346 | |||
|
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() | ||||
|
r43346 | |||
|
r37136 | def shellenviron(environ=None): | ||
"""return environ with optional override, useful for shelling out""" | ||||
|
r43346 | |||
|
r37136 | def py2shell(val): | ||
|
r44226 | """convert python object into string that is useful to shell""" | ||
|
r37136 | if val is None or val is False: | ||
|
r43347 | return b'0' | ||
|
r37136 | if val is True: | ||
|
r43347 | return b'1' | ||
|
r37136 | return pycompat.bytestr(val) | ||
|
r43346 | |||
|
r37136 | env = dict(encoding.environ) | ||
if environ: | ||||
|
r43376 | env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ)) | ||
|
r43347 | env[b'HG'] = hgexecutable() | ||
|
r37136 | return env | ||
|
r43346 | |||
|
r38510 | if pycompat.iswindows: | ||
|
r43346 | |||
|
r38510 | def shelltonative(cmd, env): | ||
|
r43779 | return platform.shelltocmdexe( # pytype: disable=module-attr | ||
cmd, shellenviron(env) | ||||
) | ||||
|
r39698 | |||
tonativestr = encoding.strfromlocal | ||||
|
r38510 | else: | ||
|
r43346 | |||
|
r38510 | def shelltonative(cmd, env): | ||
return cmd | ||||
|
r39698 | tonativestr = pycompat.identity | ||
|
r43346 | |||
|
r39698 | def tonativeenv(env): | ||
'''convert the environment from bytes to strings suitable for Popen(), etc. | ||||
''' | ||||
return pycompat.rapply(tonativestr, env) | ||||
|
r43346 | |||
|
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 | ||||
env = shellenviron(environ) | ||||
if out is None or isstdout(out): | ||||
|
r43346 | rc = subprocess.call( | ||
tonativestr(cmd), | ||||
shell=True, | ||||
close_fds=closefds, | ||||
env=tonativeenv(env), | ||||
cwd=pycompat.rapply(tonativestr, cwd), | ||||
) | ||||
|
r37136 | else: | ||
|
r43346 | proc = subprocess.Popen( | ||
tonativestr(cmd), | ||||
shell=True, | ||||
close_fds=closefds, | ||||
env=tonativeenv(env), | ||||
cwd=pycompat.rapply(tonativestr, cwd), | ||||
stdout=subprocess.PIPE, | ||||
stderr=subprocess.STDOUT, | ||||
) | ||||
|
r43347 | for line in iter(proc.stdout.readline, b''): | ||
|
r37136 | out.write(line) | ||
proc.wait() | ||||
rc = proc.returncode | ||||
|
r43347 | if pycompat.sysplatform == b'OpenVMS' and rc & 1: | ||
|
r37136 | rc = 0 | ||
return rc | ||||
|
r43346 | |||
|
r44364 | _is_gui = None | ||
def _gui(): | ||||
|
r37136 | '''Are we running in a GUI?''' | ||
if pycompat.isdarwin: | ||||
|
r43347 | if b'SSH_CONNECTION' in encoding.environ: | ||
|
r37136 | # 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: | ||||
|
r43347 | return pycompat.iswindows or encoding.environ.get(b"DISPLAY") | ||
|
r37136 | |||
|
r43346 | |||
|
r44364 | def gui(): | ||
global _is_gui | ||||
if _is_gui is None: | ||||
_is_gui = _gui() | ||||
return _is_gui | ||||
|
r37136 | 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. | ||||
""" | ||||
|
r44067 | if resourceutil.mainfrozen(): | ||
|
r44056 | if getattr(sys, 'frozen', None) == 'macosx_app': | ||
|
r37136 | # Env variable set by py2app | ||
|
r43347 | return [encoding.environ[b'EXECUTABLEPATH']] | ||
|
r37136 | else: | ||
return [pycompat.sysexecutable] | ||||
return _gethgcmd() | ||||
|
r43346 | |||
|
r37136 | 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() | ||||
|
r43346 | |||
|
r37136 | def handler(signum, frame): | ||
terminated.add(os.wait()) | ||||
|
r43346 | |||
|
r37136 | prevhandler = None | ||
SIGCHLD = getattr(signal, 'SIGCHLD', None) | ||||
if SIGCHLD is not None: | ||||
prevhandler = signal.signal(SIGCHLD, handler) | ||||
try: | ||||
pid = spawndetached(args) | ||||
while not condfn(): | ||||
|
r43346 | if (pid in terminated or not testpid(pid)) and not condfn(): | ||
|
r37136 | return -1 | ||
time.sleep(0.1) | ||||
return pid | ||||
finally: | ||||
if prevhandler is not None: | ||||
signal.signal(signal.SIGCHLD, prevhandler) | ||||
|
r38545 | |||
|
r43346 | |||
|
r38545 | @contextlib.contextmanager | ||
|
r41106 | def uninterruptible(warn): | ||
|
r38545 | """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 | ||||
|
r40532 | |||
|
r43346 | |||
|
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 | ||||
|
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 | ||||
|
r43779 | _creationflags = ( | ||
DETACHED_PROCESS | ||||
| subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr | ||||
) | ||||
|
r40532 | |||
|
r42696 | def runbgcommand( | ||
r44297 | script, | |||
env, | ||||
shell=False, | ||||
stdout=None, | ||||
stderr=None, | ||||
ensurestart=True, | ||||
record_wait=None, | ||||
|
r43346 | ): | ||
|
r40532 | '''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. | ||||
r44297 | p = subprocess.Popen( | |||
|
r40533 | tonativestr(script), | ||
|
r43346 | shell=shell, | ||
env=tonativeenv(env), | ||||
close_fds=True, | ||||
creationflags=_creationflags, | ||||
stdout=stdout, | ||||
stderr=stderr, | ||||
) | ||||
r44297 | if record_wait is not None: | |||
record_wait(p.wait) | ||||
|
r43346 | |||
|
r40532 | else: | ||
|
r43346 | |||
|
r42696 | def runbgcommand( | ||
r44297 | cmd, | |||
env, | ||||
shell=False, | ||||
stdout=None, | ||||
stderr=None, | ||||
ensurestart=True, | ||||
record_wait=None, | ||||
|
r43346 | ): | ||
r44297 | '''Spawn a command without waiting for it to finish. | |||
When `record_wait` is not None, the spawned process will not be fully | ||||
detached and the `record_wait` argument will be called with a the | ||||
`Subprocess.wait` function for the spawned process. This is mostly | ||||
useful for developers that need to make sure the spawned process | ||||
finished before a certain point. (eg: writing test)''' | ||||
|
r44364 | if pycompat.isdarwin: | ||
# avoid crash in CoreFoundation in case another thread | ||||
# calls gui() while we're calling fork(). | ||||
gui() | ||||
|
r40532 | # double-fork to completely detach from the parent process | ||
# based on http://code.activestate.com/recipes/278731 | ||||
r44297 | if record_wait is None: | |||
pid = os.fork() | ||||
if pid: | ||||
if not ensurestart: | ||||
|
r45304 | # Even though we're not waiting on the child process, | ||
# we still must call waitpid() on it at some point so | ||||
# it's not a zombie/defunct. This is especially relevant for | ||||
# chg since the parent process won't die anytime soon. | ||||
# We use a thread to make the overhead tiny. | ||||
def _do_wait(): | ||||
os.waitpid(pid, 0) | ||||
|
r45330 | |||
|
r45304 | threading.Thread(target=_do_wait, daemon=True).start() | ||
r44297 | return | |||
# 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, | ||||
b'error running %r: %s' | ||||
% (cmd, os.strerror(returncode)), | ||||
) | ||||
|
r42696 | return | ||
|
r40532 | |||
returncode = 255 | ||||
try: | ||||
r44297 | if record_wait is None: | |||
# Start a new session | ||||
os.setsid() | ||||
|
r40532 | |||
|
r43347 | stdin = open(os.devnull, b'r') | ||
|
r40532 | if stdout is None: | ||
|
r43347 | stdout = open(os.devnull, b'w') | ||
|
r40532 | if stderr is None: | ||
|
r43347 | stderr = open(os.devnull, b'w') | ||
|
r40532 | |||
# connect stdin to devnull to make sure the subprocess can't | ||||
# muck up that stream for mercurial. | ||||
r44297 | p = subprocess.Popen( | |||
|
r43346 | cmd, | ||
shell=shell, | ||||
env=env, | ||||
close_fds=True, | ||||
stdin=stdin, | ||||
stdout=stdout, | ||||
stderr=stderr, | ||||
) | ||||
r44297 | if record_wait is not None: | |||
record_wait(p.wait) | ||||
|
r40532 | returncode = 0 | ||
except EnvironmentError as ex: | ||||
|
r43346 | returncode = ex.errno & 0xFF | ||
|
r40532 | 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. | ||||
r44297 | if record_wait is None: | |||
os._exit(returncode) | ||||