| #!/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)) |