blob: cfbd65280386cce79bf059dea218a471fd60dc4a [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2018-2019 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
import errno
import fcntl
import functools
import os
import platform
import signal
import subprocess
import sys
import termios
KILL_SIGNALS = (
signal.SIGINT,
signal.SIGTERM,
signal.SIGHUP,
)
def forward_kill_signal(pid, signum, frame):
if pid == 0:
# Avoid a signal feedback loop, since signals sent to the
# process group are also sent to the current process.
signal.signal(signum, signal.SIG_DFL)
os.kill(pid, signum)
def preexec_fn(uid, gid, groups, umask):
if gid is not None:
os.setgid(gid)
if groups is not None:
os.setgroups(groups)
if uid is not None:
os.setuid(uid)
if umask is not None:
os.umask(umask)
# CPython >= 3 subprocess.Popen handles this internally.
if sys.version_info.major < 3 or platform.python_implementation() != 'CPython':
for signum in (
signal.SIGHUP,
signal.SIGINT,
signal.SIGPIPE,
signal.SIGQUIT,
signal.SIGTERM,
):
signal.signal(signum, signal.SIG_DFL)
def main(argv):
if len(argv) < 2:
return 'Usage: {} <main-child-pid> or <uid> <gid> <groups> <umask> <pass_fds> <binary> <argv0> [arg]..'.format(argv[0])
if len(argv) == 2:
# The child process is init (pid 1) in a child pid namespace, and
# the current process supervises from within the global pid namespace
# (forwarding signals to init and forwarding exit status to the parent
# process).
main_child_pid = int(argv[1])
setsid = False
proc = None
else:
# The current process is init (pid 1) in a child pid namespace.
uid, gid, groups, umask, pass_fds, binary, args = argv[1], argv[2], argv[3], argv[4], tuple(int(fd) for fd in argv[5].split(',')), argv[6], argv[7:]
uid = int(uid) if uid else None
gid = int(gid) if gid else None
groups = tuple(int(group) for group in groups.split(',')) if groups else None
umask = int(umask) if umask else None
popen_kwargs = {}
popen_kwargs['preexec_fn'] = functools.partial(preexec_fn, uid, gid, groups, umask)
if sys.version_info.major > 2:
popen_kwargs['pass_fds'] = pass_fds
# Isolate parent process from process group SIGSTOP (bug 675870)
setsid = True
os.setsid()
if sys.stdout.isatty():
try:
fcntl.ioctl(sys.stdout, termios.TIOCSCTTY, 0)
except EnvironmentError as e:
if e.errno == errno.EPERM:
# This means that stdout refers to the controlling terminal
# of the parent process, and in this case we do not want to
# steel it.
pass
else:
raise
proc = subprocess.Popen(args, executable=binary, **popen_kwargs)
main_child_pid = proc.pid
# If setsid has been called, use kill(0, signum) to
# forward signals to the entire process group.
sig_handler = functools.partial(forward_kill_signal, 0 if setsid else main_child_pid)
for signum in KILL_SIGNALS:
signal.signal(signum, sig_handler)
# wait for child processes
while True:
try:
pid, status = os.wait()
except EnvironmentError as e:
if e.errno == errno.EINTR:
continue
raise
if pid == main_child_pid:
if proc is not None:
# Suppress warning messages like this:
# ResourceWarning: subprocess 1234 is still running
proc.returncode = 0
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
signal.signal(os.WTERMSIG(status), signal.SIG_DFL)
os.kill(os.getpid(), os.WTERMSIG(status))
# go to the unreachable place
break
# this should never be reached
return 127
if __name__ == '__main__':
sys.exit(main(sys.argv))