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