#!/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: {} or [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 OSError 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 OSError 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 OSError 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))