Handle both SIGINT and SIGTERM for docker-compose up.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2015-11-10 18:52:21 -05:00
parent 9f6a5a964a
commit ea4230e7a2
3 changed files with 76 additions and 23 deletions

View File

@ -658,17 +658,24 @@ def build_log_printer(containers, service_names, monochrome):
def attach_to_logs(project, log_printer, service_names, timeout): def attach_to_logs(project, log_printer, service_names, timeout):
print("Attaching to", list_containers(log_printer.containers)) print("Attaching to", list_containers(log_printer.containers))
try:
log_printer.run()
finally:
def handler(signal, frame):
project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
def force_shutdown(signal, frame):
project.kill(service_names=service_names)
sys.exit(2)
def shutdown(signal, frame):
set_signal_handler(force_shutdown)
print("Gracefully stopping... (press Ctrl+C again to force)") print("Gracefully stopping... (press Ctrl+C again to force)")
project.stop(service_names=service_names, timeout=timeout) project.stop(service_names=service_names, timeout=timeout)
set_signal_handler(shutdown)
log_printer.run()
def set_signal_handler(handler):
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)
def list_containers(containers): def list_containers(containers):
return ", ".join(c.name for c in containers) return ", ".join(c.name for c in containers)

View File

@ -2,7 +2,9 @@ from __future__ import absolute_import
import os import os
import shlex import shlex
import signal
import subprocess import subprocess
import time
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
@ -20,6 +22,45 @@ BUILD_CACHE_TEXT = 'Using cache'
BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:latest' BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:latest'
def start_process(base_dir, options):
proc = subprocess.Popen(
['docker-compose'] + options,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=base_dir)
print("Running process: %s" % proc.pid)
return proc
def wait_on_process(proc, returncode=0):
stdout, stderr = proc.communicate()
if proc.returncode != returncode:
print(stderr)
assert proc.returncode == returncode
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
def wait_on_condition(condition, delay=0.1, timeout=5):
start_time = time.time()
while not condition():
if time.time() - start_time > timeout:
raise AssertionError("Timeout: %s" % condition)
time.sleep(delay)
class ContainerCountCondition(object):
def __init__(self, project, expected):
self.project = project
self.expected = expected
def __call__(self):
return len(self.project.containers()) == self.expected
def __str__(self):
return "waiting for counter count == %s" % self.expected
class CLITestCase(DockerClientTestCase): class CLITestCase(DockerClientTestCase):
def setUp(self): def setUp(self):
@ -42,17 +83,8 @@ class CLITestCase(DockerClientTestCase):
def dispatch(self, options, project_options=None, returncode=0): def dispatch(self, options, project_options=None, returncode=0):
project_options = project_options or [] project_options = project_options or []
proc = subprocess.Popen( proc = start_process(self.base_dir, project_options + options)
['docker-compose'] + project_options + options, return wait_on_process(proc, returncode=returncode)
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.base_dir)
print("Running process: %s" % proc.pid)
stdout, stderr = proc.communicate()
if proc.returncode != returncode:
print(stderr)
assert proc.returncode == returncode
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
def test_help(self): def test_help(self):
old_base_dir = self.base_dir old_base_dir = self.base_dir
@ -291,7 +323,7 @@ class CLITestCase(DockerClientTestCase):
returncode=1) returncode=1)
def test_up_with_timeout(self): def test_up_with_timeout(self):
self.dispatch(['up', '-d', '-t', '1'], None) self.dispatch(['up', '-d', '-t', '1'])
service = self.project.get_service('simple') service = self.project.get_service('simple')
another = self.project.get_service('another') another = self.project.get_service('another')
self.assertEqual(len(service.containers()), 1) self.assertEqual(len(service.containers()), 1)
@ -303,6 +335,20 @@ class CLITestCase(DockerClientTestCase):
self.assertFalse(config['AttachStdout']) self.assertFalse(config['AttachStdout'])
self.assertFalse(config['AttachStdin']) self.assertFalse(config['AttachStdin'])
def test_up_handles_sigint(self):
proc = start_process(self.base_dir, ['up', '-t', '2'])
wait_on_condition(ContainerCountCondition(self.project, 2))
os.kill(proc.pid, signal.SIGINT)
wait_on_condition(ContainerCountCondition(self.project, 0))
def test_up_handles_sigterm(self):
proc = start_process(self.base_dir, ['up', '-t', '2'])
wait_on_condition(ContainerCountCondition(self.project, 2))
os.kill(proc.pid, signal.SIGTERM)
wait_on_condition(ContainerCountCondition(self.project, 0))
def test_run_service_without_links(self): def test_run_service_without_links(self):
self.base_dir = 'tests/fixtures/links-composefile' self.base_dir = 'tests/fixtures/links-composefile'
self.dispatch(['run', 'console', '/bin/true']) self.dispatch(['run', 'console', '/bin/true'])

View File

@ -57,11 +57,11 @@ class CLIMainTestCase(unittest.TestCase):
with mock.patch('compose.cli.main.signal', autospec=True) as mock_signal: with mock.patch('compose.cli.main.signal', autospec=True) as mock_signal:
attach_to_logs(project, log_printer, service_names, timeout) attach_to_logs(project, log_printer, service_names, timeout)
mock_signal.signal.assert_called_once_with(mock_signal.SIGINT, mock.ANY) assert mock_signal.signal.mock_calls == [
mock.call(mock_signal.SIGINT, mock.ANY),
mock.call(mock_signal.SIGTERM, mock.ANY),
]
log_printer.run.assert_called_once_with() log_printer.run.assert_called_once_with()
project.stop.assert_called_once_with(
service_names=service_names,
timeout=timeout)
class SetupConsoleHandlerTestCase(unittest.TestCase): class SetupConsoleHandlerTestCase(unittest.TestCase):