procutil.py
790 lines
| 22.1 KiB
| text/x-python
|
PythonLexer
|
r37136 | # procutil.py - utility for managing processes and executable environment | ||
# | ||||
# Copyright 2005 K. Thananchayan <thananck@yahoo.com> | ||||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r37136 | # 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. | ||||
|
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 | |||
|
r45587 | if pycompat.iswindows: | ||
from .. import windows as platform | ||||
else: | ||||
from .. import posix as platform | ||||
|
r43346 | |||
|
r37136 | def isatty(fp): | ||
try: | ||||
return fp.isatty() | ||||
except AttributeError: | ||||
return False | ||||
|
r43346 | |||
|
r46790 | class BadFile(io.RawIOBase): | ||
"""Dummy file object to simulate closed stdio behavior""" | ||||
def readinto(self, b): | ||||
raise IOError(errno.EBADF, 'Bad file descriptor') | ||||
def write(self, b): | ||||
raise IOError(errno.EBADF, 'Bad file descriptor') | ||||
|
r49801 | class LineBufferedWrapper: | ||
|
r45584 | def __init__(self, orig): | ||
self.orig = orig | ||||
|
r45477 | |||
|
r45584 | def __getattr__(self, attr): | ||
return getattr(self.orig, attr) | ||||
|
r45477 | |||
|
r45584 | def write(self, s): | ||
orig = self.orig | ||||
res = orig.write(s) | ||||
if s.endswith(b'\n'): | ||||
orig.flush() | ||||
return res | ||||
|
r45477 | |||
|
r45584 | |||
|
r49318 | # pytype: disable=attribute-error | ||
|
r45584 | io.BufferedIOBase.register(LineBufferedWrapper) | ||
|
r49318 | # pytype: enable=attribute-error | ||
|
r45477 | |||
|
r45585 | def make_line_buffered(stream): | ||
|
r50273 | # First, check if we need to wrap the stream. | ||
check_stream = stream | ||||
while True: | ||||
if isinstance(check_stream, WriteAllWrapper): | ||||
check_stream = check_stream.orig | ||||
elif pycompat.iswindows and isinstance( | ||||
check_stream, | ||||
# pytype: disable=module-attr | ||||
platform.winstdout | ||||
# pytype: enable=module-attr | ||||
): | ||||
check_stream = check_stream.fp | ||||
else: | ||||
break | ||||
if isinstance(check_stream, io.RawIOBase): | ||||
# The stream is unbuffered, we don't need to emulate line buffering. | ||||
|
r45585 | return stream | ||
|
r50273 | elif isinstance(check_stream, io.BufferedIOBase): | ||
# The stream supports some kind of buffering. We can't assume that | ||||
# lines are flushed. Fall back to wrapping the stream. | ||||
pass | ||||
else: | ||||
raise NotImplementedError( | ||||
"can't determine whether stream is buffered or not" | ||||
) | ||||
|
r45585 | if isinstance(stream, LineBufferedWrapper): | ||
return stream | ||||
return LineBufferedWrapper(stream) | ||||
|
r46452 | def unwrap_line_buffered(stream): | ||
if isinstance(stream, LineBufferedWrapper): | ||||
assert not isinstance(stream.orig, LineBufferedWrapper) | ||||
return stream.orig | ||||
return stream | ||||
|
r49801 | class WriteAllWrapper: | ||
|
r45655 | def __init__(self, orig): | ||
self.orig = orig | ||||
def __getattr__(self, attr): | ||||
return getattr(self.orig, attr) | ||||
def write(self, s): | ||||
write1 = self.orig.write | ||||
m = memoryview(s) | ||||
total_to_write = len(s) | ||||
total_written = 0 | ||||
while total_written < total_to_write: | ||||
|
r49958 | c = write1(m[total_written:]) | ||
if c: | ||||
total_written += c | ||||
|
r45655 | return total_written | ||
|
r49318 | # pytype: disable=attribute-error | ||
|
r45655 | io.IOBase.register(WriteAllWrapper) | ||
|
r49318 | # pytype: enable=attribute-error | ||
|
r45655 | |||
|
r45663 | def _make_write_all(stream): | ||
|
r45655 | if isinstance(stream, WriteAllWrapper): | ||
return stream | ||||
if isinstance(stream, io.BufferedIOBase): | ||||
# The io.BufferedIOBase.write() contract guarantees that all data is | ||||
# written. | ||||
return stream | ||||
# In general, the write() method of streams is free to write only part of | ||||
# the data. | ||||
return WriteAllWrapper(stream) | ||||
|
r49765 | # Python 3 implements its own I/O streams. Unlike stdio of C library, | ||
# sys.stdin/stdout/stderr may be None if underlying fd is closed. | ||||
|
r46699 | |||
|
r49765 | # TODO: .buffer might not exist if std streams were replaced; we'll need | ||
# a silly wrapper to make a bytes stream backed by a unicode one. | ||||
|
r46791 | |||
|
r49765 | if sys.stdin is None: | ||
stdin = BadFile() | ||||
else: | ||||
stdin = sys.stdin.buffer | ||||
if sys.stdout is None: | ||||
stdout = BadFile() | ||||
|
r45599 | else: | ||
|
r49765 | stdout = _make_write_all(sys.stdout.buffer) | ||
if sys.stderr is None: | ||||
stderr = BadFile() | ||||
else: | ||||
stderr = _make_write_all(sys.stderr.buffer) | ||||
|
r37136 | |||
|
r49765 | if pycompat.iswindows: | ||
# Work around Windows bugs. | ||||
stdout = platform.winstdout(stdout) # pytype: disable=module-attr | ||||
stderr = platform.winstdout(stderr) # pytype: disable=module-attr | ||||
if isatty(stdout): | ||||
# The standard library doesn't offer line-buffered binary streams. | ||||
stdout = make_line_buffered(stdout) | ||||
|
r37136 | |||
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 | |||
|
r49801 | class _pfile: | ||
|
r37477 | """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): | ||
|
r46554 | """filter string S through a pair of temporary files with CMD. | ||
|
r37136 | 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 | ||||
|
r46554 | the temporary files generated.""" | ||
|
r37136 | 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""" | ||||
|
r49768 | for name, fn in _filtertable.items(): | ||
|
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: | ||||
|
r49768 | env.update((k, py2shell(v)) for k, v in environ.items()) | ||
|
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): | ||
|
r46554 | """convert the environment from bytes to strings suitable for Popen(), etc.""" | ||
|
r39698 | return pycompat.rapply(tonativestr, env) | ||
|
r43346 | |||
|
r37136 | def system(cmd, environ=None, cwd=None, out=None): | ||
|
r46554 | """enhanced shell command execution. | ||
|
r37136 | 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 | ||||
|
r46554 | write() method. stdout and stderr will be redirected to out.""" | ||
|
r37136 | 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: | ||||
|
r47201 | return ( | ||
pycompat.iswindows | ||||
or encoding.environ.get(b"DISPLAY") | ||||
or encoding.environ.get(b"WAYLAND_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, | ||||
r46371 | stdin_bytes=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. | ||||
r46371 | ||||
try: | ||||
stdin = None | ||||
if stdin_bytes is not None: | ||||
stdin = pycompat.unnamedtempfile() | ||||
stdin.write(stdin_bytes) | ||||
stdin.flush() | ||||
stdin.seek(0) | ||||
p = subprocess.Popen( | ||||
|
r46644 | pycompat.rapply(tonativestr, script), | ||
r46371 | shell=shell, | |||
env=tonativeenv(env), | ||||
close_fds=True, | ||||
creationflags=_creationflags, | ||||
stdin=stdin, | ||||
stdout=stdout, | ||||
stderr=stderr, | ||||
) | ||||
if record_wait is not None: | ||||
record_wait(p.wait) | ||||
finally: | ||||
if stdin is not None: | ||||
stdin.close() | ||||
|
r43346 | |||
|
r40532 | else: | ||
|
r43346 | |||
|
r49765 | def runbgcommand( | ||
|
r47651 | cmd, | ||
env, | ||||
shell=False, | ||||
stdout=None, | ||||
stderr=None, | ||||
ensurestart=True, | ||||
record_wait=None, | ||||
stdin_bytes=None, | ||||
): | ||||
"""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)""" | ||||
if pycompat.isdarwin: | ||||
# avoid crash in CoreFoundation in case another thread | ||||
# calls gui() while we're calling fork(). | ||||
gui() | ||||
if shell: | ||||
script = cmd | ||||
else: | ||||
if isinstance(cmd, bytes): | ||||
cmd = [cmd] | ||||
script = b' '.join(shellquote(x) for x in cmd) | ||||
if record_wait is None: | ||||
# double-fork to completely detach from the parent process | ||||
script = b'( %s ) &' % script | ||||
start_new_session = True | ||||
else: | ||||
start_new_session = False | ||||
ensurestart = True | ||||
|
r49319 | stdin = None | ||
|
r47651 | try: | ||
if stdin_bytes is None: | ||||
stdin = subprocess.DEVNULL | ||||
else: | ||||
stdin = pycompat.unnamedtempfile() | ||||
stdin.write(stdin_bytes) | ||||
stdin.flush() | ||||
stdin.seek(0) | ||||
if stdout is None: | ||||
stdout = subprocess.DEVNULL | ||||
if stderr is None: | ||||
stderr = subprocess.DEVNULL | ||||
p = subprocess.Popen( | ||||
script, | ||||
shell=True, | ||||
env=env, | ||||
close_fds=True, | ||||
stdin=stdin, | ||||
stdout=stdout, | ||||
stderr=stderr, | ||||
start_new_session=start_new_session, | ||||
) | ||||
except Exception: | ||||
if record_wait is not None: | ||||
record_wait(255) | ||||
raise | ||||
finally: | ||||
|
r49319 | if stdin_bytes is not None and stdin is not None: | ||
|
r49318 | assert not isinstance(stdin, int) | ||
|
r47651 | stdin.close() | ||
if not ensurestart: | ||||
# 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. | ||||
t = threading.Thread(target=lambda: p.wait) | ||||
t.daemon = True | ||||
t.start() | ||||
else: | ||||
returncode = p.wait | ||||
if record_wait is not None: | ||||
record_wait(returncode) | ||||