Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Stop container when signals captured #478

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
import atexit
import functools as ft
import os
import signal
import urllib
from os.path import exists
from pathlib import Path
from typing import Optional, Union
from typing import Optional, Union, Callable, Iterable, Optional

import docker
from docker.errors import NotFound
Expand Down Expand Up @@ -69,6 +70,11 @@ def run(
)
if detach:
atexit.register(_stop_container, container)
signal_handler = ft.partial(_stop_container, container)
_register_signal_handler(
signal_handler,
{signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, signal.SIGHUP}
)
return container

def port(self, container_id: str, port: int) -> int:
Expand Down Expand Up @@ -154,3 +160,28 @@ def _stop_container(container: Container) -> None:
pass
except Exception as ex:
LOGGER.warning("failed to shut down container %s with image %s: %s", container.id, container.image, ex)


def _register_signal_handler(handler: Callable[[], None], signals: Iterable) -> None:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more advanced version of signal.signal that does not overwrite existing signal handler registered to signal.

def signal_wrapper(old_handler: Optional[Callable[[], None]] = None):
if old_handler is not None:
old_handler()
handler()

for sig in signals:
# Register function for this signal and pop() the previously
# registered one (if any). This can either be a callable,
# SIG_IGN (ignore signal) or SIG_DFL (perform default action
# for signal).
old_handler = signal.getsignal(sig)
if old_handler in (signal.SIG_DFL, signal.SIG_IGN) or not callable(old_handler):
continue
# This is needed otherwise we'll get a KeyboardInterrupt
# strace on interpreter exit, even if the process exited
# with sig 0.
if (sig == signal.SIGINT and
old_handler is signal.default_int_handler):
continue
wrapped_handler = ft.partial(signal_wrapper, old_handler)
signal.signal(sig, wrapped_handler)

Loading