in src/buildstream/sandbox/_sandboxbuildboxrun.py [0:0]
def _run_buildbox(self, argv, stdin, stdout, stderr, *, interactive):
def kill_proc():
if process:
# First attempt to gracefully terminate
proc = psutil.Process(process.pid)
proc.terminate()
try:
proc.wait(15)
except psutil.TimeoutExpired:
utils._kill_process_tree(process.pid)
def suspend_proc():
group_id = os.getpgid(process.pid)
os.killpg(group_id, signal.SIGSTOP)
def resume_proc():
group_id = os.getpgid(process.pid)
os.killpg(group_id, signal.SIGCONT)
with ExitStack() as stack:
# We want to launch buildbox-run in a new session in non-interactive
# mode so that we handle the SIGTERM and SIGTSTP signals separately
# from the nested process, but in interactive mode this causes
# launched shells to lack job control as the signals don't reach
# the shell process.
#
if interactive:
new_session = False
else:
new_session = True
stack.enter_context(_signals.suspendable(suspend_proc, resume_proc))
stack.enter_context(_signals.terminator(kill_proc))
process = subprocess.Popen( # pylint: disable=consider-using-with
argv,
close_fds=True,
stdin=stdin,
stdout=stdout,
stderr=stderr,
start_new_session=new_session,
)
# Wait for the child process to finish, ensuring that
# a SIGINT has exactly the effect the user probably
# expects (i.e. let the child process handle it).
try:
while True:
try:
# Here, we don't use `process.wait()` directly without a timeout
# This is because, if we were to do that, and the process would never
# output anything, the control would never be given back to the python
# process, which might thus not be able to check for request to
# shutdown, or kill the process.
# We therefore loop with a timeout, to ensure the python process
# can act if it needs.
returncode = process.wait(timeout=1)
# If the process exits due to a signal, we
# brutally murder it to avoid zombies
if returncode < 0:
utils._kill_process_tree(process.pid)
except subprocess.TimeoutExpired:
continue
# Unlike in the bwrap case, here only the main
# process seems to receive the SIGINT. We pass
# on the signal to the child and then continue
# to wait.
except _signals.TerminateException:
process.send_signal(signal.SIGINT)
continue
break
# If we can't find the process, it has already died of
# its own accord, and therefore we don't need to check
# or kill anything.
except psutil.NoSuchProcess:
pass
if interactive and stdin.isatty():
# Make this process the foreground process again, otherwise the
# next read() on stdin will trigger SIGTTIN and stop the process.
# This is required because the sandboxed process does not have
# permission to do this on its own (running in separate PID namespace).
#
# tcsetpgrp() will trigger SIGTTOU when called from a background
# process, so ignore it temporarily.
handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
os.tcsetpgrp(0, os.getpid())
signal.signal(signal.SIGTTOU, handler)
if returncode != 0:
raise SandboxError("buildbox-run failed with returncode {}".format(returncode))