File: child_processes_test.py

package info (click to toggle)
dumb-init 1.2.5-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 268 kB
  • sloc: python: 677; ansic: 260; makefile: 86; sh: 49
file content (149 lines) | stat: -rw-r--r-- 4,500 bytes parent folder | download
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
    )