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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
import os
import re
import signal
import sys
from subprocess import PIPE
from subprocess import Popen
import pytest
from testing import is_alive
from testing import kill_if_alive
from testing import pid_tree
from testing import sleep_until
def spawn_and_kill_pipeline():
proc = Popen((
'dumb-init',
'sh', '-c',
"yes 'oh, hi' | tail & yes error | tail >&2",
))
def assert_living_pids():
assert len(living_pids(pid_tree(os.getpid()))) == 6
sleep_until(assert_living_pids)
pids = pid_tree(os.getpid())
proc.send_signal(signal.SIGTERM)
proc.wait()
return pids
def living_pids(pids):
return {pid for pid in pids if is_alive(pid)}
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
def test_setsid_signals_entire_group():
"""When dumb-init is running in setsid mode, it should signal the entire
process group rooted at it.
"""
pids = spawn_and_kill_pipeline()
def assert_no_living_pids():
assert len(living_pids(pids)) == 0
sleep_until(assert_no_living_pids)
@pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled')
def test_no_setsid_doesnt_signal_entire_group():
"""When dumb-init is not running in setsid mode, it should only signal its
immediate child.
"""
pids = spawn_and_kill_pipeline()
def assert_four_living_pids():
assert len(living_pids(pids)) == 4
sleep_until(assert_four_living_pids)
for pid in living_pids(pids):
kill_if_alive(pid)
def spawn_process_which_dies_with_children():
"""Spawn a process which spawns some children and then dies without
signaling them, wrapped in dumb-init.
Returns a tuple (child pid, child stdout pipe), where the child is
print_signals. This is useful because you can signal the PID and see if
anything gets printed onto the stdout pipe.
"""
proc = Popen(
(
'dumb-init',
'sh', '-c',
# we need to sleep before the shell exits, or dumb-init might send
# TERM to print_signals before it has had time to register custom
# signal handlers
'{python} -m testing.print_signals & sleep 1'.format(
python=sys.executable,
),
),
stdout=PIPE,
)
proc.wait()
assert proc.returncode == 0
# read a line from print_signals, figure out its pid
line = proc.stdout.readline()
match = re.match(b'ready \\(pid: ([0-9]+)\\)\n', line)
assert match, line
child_pid = int(match.group(1))
# at this point, the shell and dumb-init have both exited, but
# print_signals may or may not still be running (depending on whether
# setsid mode is enabled)
return child_pid, proc.stdout
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
def test_all_processes_receive_term_on_exit_if_setsid():
"""If the child exits for some reason, dumb-init should send TERM to all
processes in its session if setsid mode is enabled."""
child_pid, child_stdout = spawn_process_which_dies_with_children()
# print_signals should have received TERM
assert child_stdout.readline() == b'15\n'
os.kill(child_pid, signal.SIGKILL)
@pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled')
def test_processes_dont_receive_term_on_exit_if_no_setsid():
"""If the child exits for some reason, dumb-init should not send TERM to
any other processes if setsid mode is disabled."""
child_pid, child_stdout = spawn_process_which_dies_with_children()
# print_signals should not have received TERM; to test this, we send it
# some other signals and ensure they were received (and TERM wasn't)
for signum in [1, 2, 3]:
os.kill(child_pid, signum)
assert child_stdout.readline() == str(signum).encode('ascii') + b'\n'
os.kill(child_pid, signal.SIGKILL)
@pytest.mark.parametrize(
'args', [
('/doesnotexist',),
('--', '/doesnotexist'),
('-c', '/doesnotexist'),
('--single-child', '--', '/doesnotexist'),
],
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_fails_nonzero_with_bad_exec(args):
"""If dumb-init can't exec as requested, it should exit nonzero."""
proc = Popen(('dumb-init',) + args, stderr=PIPE)
_, stderr = proc.communicate()
assert proc.returncode != 0
assert (
b'[dumb-init] /doesnotexist: No such file or directory\n'
in stderr
)
|