aboutsummaryrefslogtreecommitdiff
blob: cfbd65280386cce79bf059dea218a471fd60dc4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/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))