blob: b9bf56a7b1473155a21d0d47034fcf8cb4c5b08b [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2018-2021 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
from pathlib import Path
KILL_SIGNALS = (
signal.SIGINT,
signal.SIGTERM,
signal.SIGHUP,
)
SIGTSTP_SIGCONT = (
signal.SIGTSTP,
signal.SIGCONT,
)
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 forward_sigtstp_sigcont(pid, signum, frame):
handler = None
if pid == 0:
# Temporarily disable the handler in order to prevent it from
# being called recursively, since the signal will also be sent
# to the current process.
handler = signal.signal(signum, signal.SIG_DFL)
os.kill(pid, signum)
if handler is not None:
signal.signal(signum, handler)
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 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 = {
'preexec_fn': functools.partial(preexec_fn, uid, gid, groups, umask),
'pass_fds': pass_fds,
}
# Obtain the current nice value, which will be potentially be
# used as the newly created session's autogroup nice value.
nice_value = os.nice(0)
# Isolate parent process from process group SIGSTOP (bug 675870)
setsid = True
os.setsid()
# Set the previously obtained autogroup nice value again,
# since we created a new session with os.setsid() above.
try:
Path("/proc/self/autogroup").write_text(str(nice_value))
except EnvironmentError as e:
# The process is likely not allowed to set the autogroup
# value (Linux employs a rate limiting for unprivileged
# changes to the autogroup value) or autogroups are not
# enabled. Nothing we can do here, so we simply carry on.
pass
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
# steal 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)
# For correct operation of Ctrl+Z, forward SIGTSTP and SIGCONT.
sigtstp_sigcont_handler = functools.partial(forward_sigtstp_sigcont, 0 if setsid else main_child_pid)
for signum in SIGTSTP_SIGCONT:
signal.signal(signum, sigtstp_sigcont_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))