Merge pull request #25 from chriskuehl/background-support
Properly respond to job control signals
This commit is contained in:
commit
2aa56d6aab
5 changed files with 168 additions and 7 deletions
51
dumb-init.c
51
dumb-init.c
|
@ -37,9 +37,60 @@ void forward_signal(int signum) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The dumb-init signal handler.
|
||||||
|
*
|
||||||
|
* The main job of this signal handler is to forward signals along to our child
|
||||||
|
* process(es). In setsid mode, this means signaling the entire process group
|
||||||
|
* rooted at our child. In non-setsid mode, this is just signaling the primary
|
||||||
|
* child.
|
||||||
|
*
|
||||||
|
* In most cases, simply proxying the received signal is sufficient. If we
|
||||||
|
* receive a job control signal, however, we should not only forward it, but
|
||||||
|
* also sleep dumb-init itself.
|
||||||
|
*
|
||||||
|
* This allows users to run foreground processes using dumb-init and to
|
||||||
|
* control them using normal shell job control features (e.g. Ctrl-Z to
|
||||||
|
* generate a SIGTSTP and suspend the process).
|
||||||
|
*
|
||||||
|
* The libc manual is useful:
|
||||||
|
* https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html
|
||||||
|
*
|
||||||
|
* When running in setsid mode, however, it is not sufficient to forward
|
||||||
|
* SIGTSTP/SIGTTIN/SIGTTOU in most cases. If the process has not added a custom
|
||||||
|
* signal handler for these signals, then the kernel will not apply default
|
||||||
|
* signal handling behavior (which would be suspending the process) since it is
|
||||||
|
* a member of an orphaned process group.
|
||||||
|
*
|
||||||
|
* Sadly this doesn't appear to be well documented except in the kernel itself:
|
||||||
|
* https://github.com/torvalds/linux/blob/v4.2/kernel/signal.c#L2296-L2299
|
||||||
|
*
|
||||||
|
* Forwarding SIGSTOP instead is effective, though not ideal; unlike SIGTSTP,
|
||||||
|
* SIGSTOP cannot be caught, and so it doesn't allow processes a change to
|
||||||
|
* clean up before suspending. In non-setsid mode, we proxy the original signal
|
||||||
|
* instead of SIGSTOP for this reason.
|
||||||
|
*/
|
||||||
void handle_signal(int signum) {
|
void handle_signal(int signum) {
|
||||||
DEBUG("Received signal %d.\n", signum);
|
DEBUG("Received signal %d.\n", signum);
|
||||||
|
|
||||||
|
if (
|
||||||
|
signum == SIGTSTP || // tty: background yourself
|
||||||
|
signum == SIGTTIN || // tty: stop reading
|
||||||
|
signum == SIGTTOU // tty: stop writing
|
||||||
|
) {
|
||||||
|
if (use_setsid) {
|
||||||
|
DEBUG("Running in setsid mode, so forwarding SIGSTOP instead.\n");
|
||||||
|
forward_signal(SIGSTOP);
|
||||||
|
} else {
|
||||||
|
DEBUG("Not running in setsid mode, so forwarding the original signal (%d).\n", signum);
|
||||||
forward_signal(signum);
|
forward_signal(signum);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG("Suspending self due to TTY signal.\n");
|
||||||
|
kill(getpid(), SIGSTOP);
|
||||||
|
} else {
|
||||||
|
forward_signal(signum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void print_help(char *argv[]) {
|
void print_help(char *argv[]) {
|
||||||
|
|
|
@ -11,10 +11,14 @@ import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from tests.lib.testing import CATCHABLE_SIGNALS
|
|
||||||
|
CATCHABLE_SIGNALS = frozenset(
|
||||||
|
set(range(1, 32)) - set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
print_queue = []
|
print_queue = []
|
||||||
|
last_signal = None
|
||||||
|
|
||||||
|
|
||||||
def unbuffered_print(line):
|
def unbuffered_print(line):
|
||||||
|
@ -35,6 +39,12 @@ if __name__ == '__main__':
|
||||||
# loop forever just printing signals
|
# loop forever just printing signals
|
||||||
while True:
|
while True:
|
||||||
if print_queue:
|
if print_queue:
|
||||||
unbuffered_print(print_queue.pop())
|
signum = print_queue.pop()
|
||||||
|
unbuffered_print(signum)
|
||||||
|
|
||||||
|
if signum == signal.SIGINT and last_signal == signal.SIGINT:
|
||||||
|
print('Received SIGINT twice, exiting.')
|
||||||
|
exit(0)
|
||||||
|
last_signal = signum
|
||||||
|
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from py._path.local import LocalPath
|
from py._path.local import LocalPath
|
||||||
|
|
||||||
|
|
||||||
CATCHABLE_SIGNALS = frozenset(
|
# these signals cause dumb-init to suspend itself
|
||||||
set(range(1, 32)) - set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD])
|
SUSPEND_SIGNALS = frozenset([
|
||||||
|
signal.SIGTSTP,
|
||||||
|
signal.SIGTTOU,
|
||||||
|
signal.SIGTTIN,
|
||||||
|
])
|
||||||
|
|
||||||
|
NORMAL_SIGNALS = frozenset(
|
||||||
|
set(range(1, 32))
|
||||||
|
- set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD])
|
||||||
|
- SUSPEND_SIGNALS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,3 +42,10 @@ def pid_tree(pid):
|
||||||
def is_alive(pid):
|
def is_alive(pid):
|
||||||
"""Return whether a process is running with the given PID."""
|
"""Return whether a process is running with the given PID."""
|
||||||
return LocalPath('/proc').join(str(pid)).isdir()
|
return LocalPath('/proc').join(str(pid)).isdir()
|
||||||
|
|
||||||
|
|
||||||
|
def process_state(pid):
|
||||||
|
"""Return a process' state, such as "stopped" or "running"."""
|
||||||
|
status = LocalPath('/proc').join(str(pid), 'status').read()
|
||||||
|
m = re.search('^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
|
||||||
|
return m.group(1)
|
||||||
|
|
|
@ -5,11 +5,12 @@ import sys
|
||||||
from subprocess import PIPE
|
from subprocess import PIPE
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
from tests.lib.testing import CATCHABLE_SIGNALS
|
from tests.lib.testing import NORMAL_SIGNALS
|
||||||
from tests.lib.testing import pid_tree
|
from tests.lib.testing import pid_tree
|
||||||
|
|
||||||
|
|
||||||
def test_prints_signals(both_debug_modes, both_setsid_modes):
|
def test_prints_signals(both_debug_modes, both_setsid_modes):
|
||||||
|
"""Ensure dumb-init proxies regular signals to its child."""
|
||||||
proc = Popen(
|
proc = Popen(
|
||||||
('dumb-init', sys.executable, '-m', 'tests.lib.print_signals'),
|
('dumb-init', sys.executable, '-m', 'tests.lib.print_signals'),
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
|
@ -17,7 +18,7 @@ def test_prints_signals(both_debug_modes, both_setsid_modes):
|
||||||
|
|
||||||
assert re.match(b'^ready \(pid: (?:[0-9]+)\)\n$', proc.stdout.readline())
|
assert re.match(b'^ready \(pid: (?:[0-9]+)\)\n$', proc.stdout.readline())
|
||||||
|
|
||||||
for signum in CATCHABLE_SIGNALS:
|
for signum in NORMAL_SIGNALS:
|
||||||
proc.send_signal(signum)
|
proc.send_signal(signum)
|
||||||
assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
|
assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
|
||||||
|
|
||||||
|
|
82
tests/shell_background_test.py
Normal file
82
tests/shell_background_test.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from signal import SIGCONT
|
||||||
|
from signal import SIGKILL
|
||||||
|
from subprocess import PIPE
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
|
from tests.lib.testing import pid_tree
|
||||||
|
from tests.lib.testing import process_state
|
||||||
|
from tests.lib.testing import SUSPEND_SIGNALS
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_background_support_setsid(both_debug_modes, setsid_enabled):
|
||||||
|
"""In setsid mode, dumb-init should suspend itself and its children when it
|
||||||
|
receives SIGTSTP, SIGTTOU, or SIGTTIN.
|
||||||
|
"""
|
||||||
|
proc = Popen(
|
||||||
|
('dumb-init', sys.executable, '-m', 'tests.lib.print_signals'),
|
||||||
|
stdout=PIPE,
|
||||||
|
)
|
||||||
|
match = re.match(b'^ready \(pid: ([0-9]+)\)\n$', proc.stdout.readline())
|
||||||
|
pid = match.group(1).decode('ascii')
|
||||||
|
|
||||||
|
for signum in SUSPEND_SIGNALS:
|
||||||
|
# both dumb-init and print_signals should be running or sleeping
|
||||||
|
assert process_state(pid) in ['running', 'sleeping']
|
||||||
|
assert process_state(proc.pid) in ['running', 'sleeping']
|
||||||
|
|
||||||
|
# both should now suspend
|
||||||
|
proc.send_signal(signum)
|
||||||
|
|
||||||
|
for _ in range(1000):
|
||||||
|
time.sleep(0.001)
|
||||||
|
try:
|
||||||
|
assert process_state(proc.pid) == 'stopped'
|
||||||
|
assert process_state(pid) == 'stopped'
|
||||||
|
except AssertionError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Timed out waiting for processes to stop.')
|
||||||
|
|
||||||
|
# and then both wake up again
|
||||||
|
proc.send_signal(SIGCONT)
|
||||||
|
assert (
|
||||||
|
proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
|
||||||
|
)
|
||||||
|
assert process_state(pid) in ['running', 'sleeping']
|
||||||
|
assert process_state(proc.pid) in ['running', 'sleeping']
|
||||||
|
|
||||||
|
for pid in pid_tree(proc.pid):
|
||||||
|
os.kill(pid, SIGKILL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prints_signals(both_debug_modes, setsid_disabled):
|
||||||
|
"""In non-setsid mode, dumb-init should forward the signals SIGTSTP,
|
||||||
|
SIGTTOU, and SIGTTIN, and then suspend itself.
|
||||||
|
"""
|
||||||
|
proc = Popen(
|
||||||
|
('dumb-init', sys.executable, '-m', 'tests.lib.print_signals'),
|
||||||
|
stdout=PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert re.match(b'^ready \(pid: (?:[0-9]+)\)\n$', proc.stdout.readline())
|
||||||
|
|
||||||
|
for signum in SUSPEND_SIGNALS:
|
||||||
|
assert process_state(proc.pid) in ['running', 'sleeping']
|
||||||
|
proc.send_signal(signum)
|
||||||
|
assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
|
||||||
|
assert process_state(proc.pid) == 'stopped'
|
||||||
|
|
||||||
|
proc.send_signal(SIGCONT)
|
||||||
|
assert (
|
||||||
|
proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
|
||||||
|
)
|
||||||
|
assert process_state(proc.pid) in ['running', 'sleeping']
|
||||||
|
|
||||||
|
for pid in pid_tree(proc.pid):
|
||||||
|
os.kill(pid, SIGKILL)
|
Loading…
Reference in a new issue