diff --git a/.travis.yml b/.travis.yml index 7bc25f9..425b695 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,10 @@ addons: - git - gdb - valgrind + - python-dev + - libcap-dev + - python-pip + - python-virtualenv script: ./ci/run_build.sh diff --git a/Dockerfile b/Dockerfile index cf3eed1..8c688c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM ubuntu:precise RUN apt-get update \ - && apt-get install --no-install-recommends --yes build-essential git gdb valgrind cmake rpm python3 \ + && apt-get install --no-install-recommends --yes build-essential git gdb valgrind cmake rpm python-dev libcap-dev python-pip python-virtualenv \ && rm -rf /var/lib/apt/lists/* + +RUN pip install psutil diff --git a/ci/run_build.sh b/ci/run_build.sh index ffc4b20..a21011a 100755 --- a/ci/run_build.sh +++ b/ci/run_build.sh @@ -3,13 +3,23 @@ set -o errexit set -o nounset +# Paths : ${SOURCE_DIR:="."} : ${DIST_DIR:="${SOURCE_DIR}/dist"} : ${BUILD_DIR:="/tmp/build"} +# Make those paths absolute, and export them for the Python tests to consume. +export SOURCE_DIR="$(readlink -f "${SOURCE_DIR}")" +export DIST_DIR="$(readlink -f "${DIST_DIR}")" +export BUILD_DIR="$(readlink -f "${BUILD_DIR}")" + + +# Ensure Python output is not buffered (to make tests output clearer) +export PYTHONUNBUFFERED=1 + # Set path to prioritize our utils export REAL_PATH="${PATH}" -export PATH="$(readlink -f "${SOURCE_DIR}")/ci/util:${PATH}" +export PATH="${SOURCE_DIR}/ci/util:${PATH}" # Build cmake -B"${BUILD_DIR}" -H"${SOURCE_DIR}" @@ -46,3 +56,29 @@ for tini in "${BUILD_DIR}/tini" "${BUILD_DIR}/tini-static"; do dpkg --contents "${DIST_DIR}/tini"*deb fi done + +# Create virtual environment to run tests +VENV="${BUILD_DIR}/venv" +virtualenv "${VENV}" + +# Don't use activate because it does not play nice with nounset +export PATH="${VENV}/bin:${PATH}" + +# Install test dependencies + +# We need a patched version because Travis only gives us Ubuntu Precise +# (whose Linux headers don't include PR_SET_CHILD_SUBREAPER), but actually +# runs a newer Linux Kernel (because we're actually in Docker) that has the +# PR_SET_CHILD_SUBREAPER prctl call. +pushd /tmp +pip install python-prctl==1.6.1 --download="." +tar -xvf /tmp/python-prctl-1.6.1.tar.gz +cd python-prctl-1.6.1 +patch -p1 < "${SOURCE_DIR}/test/0001-Add-PR_SET_CHILD_SUBREAPER.patch" +python setup.py install +popd + +pip install psutil + +# Run tests +python "${SOURCE_DIR}/test/run_inner_tests.py" diff --git a/dtest.sh b/dtest.sh index 07fcfbf..00e0894 100755 --- a/dtest.sh +++ b/dtest.sh @@ -6,4 +6,4 @@ IMG="tini" docker build -t "${IMG}" . -python test/test.py "${IMG}" +python test/run_outer_tests.py "${IMG}" diff --git a/test/0001-Add-PR_SET_CHILD_SUBREAPER.patch b/test/0001-Add-PR_SET_CHILD_SUBREAPER.patch new file mode 100644 index 0000000..f2f339b --- /dev/null +++ b/test/0001-Add-PR_SET_CHILD_SUBREAPER.patch @@ -0,0 +1,35 @@ +From b8c6ccd4575837e3901bbdee7b219ef951dc2065 Mon Sep 17 00:00:00 2001 +From: Thomas Orozco +Date: Sun, 28 Jun 2015 15:25:37 +0200 +Subject: [PATCH] Add PR_SET_CHILD_SUBREAPER + +--- + _prctlmodule.c | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/_prctlmodule.c b/_prctlmodule.c +index 14121c3..19ad141 100644 +--- a/_prctlmodule.c ++++ b/_prctlmodule.c +@@ -15,6 +15,18 @@ + #include + #include + ++/* Our builds run in a Docker environment that has those, but they are ++ * not in the kernel headers. Add them. ++ */ ++ ++#ifndef PR_SET_CHILD_SUBREAPER ++#define PR_SET_CHILD_SUBREAPER 36 ++#endif ++ ++#ifndef PR_GET_CHILD_SUBREAPER ++#define PR_GET_CHILD_SUBREAPER 37 ++#endif ++ + /* New in 2.6.32, but named and implemented inconsistently. The linux + * implementation has two ways of setting the policy to the default, and thus + * needs an extra argument. We ignore the first argument and always call +-- +2.4.3 + diff --git a/test/reaping/stage_1.py b/test/reaping/stage_1.py index d8dba2f..de39b66 100755 --- a/test/reaping/stage_1.py +++ b/test/reaping/stage_1.py @@ -1,25 +1,34 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +from __future__ import print_function import os import subprocess import time +import psutil -if __name__ == "__main__": + +def main(): p = subprocess.Popen([os.path.join(os.path.dirname(__file__), "stage_2.py")]) p.wait() - # These are the only PIDs that should remain if the system is well-behaved: - # - This process - # - Init - expected_pids = [1, os.getpid()] + # In tests, we assume this process is the direct child of init + this_process = psutil.Process(os.getpid()) + init_process = this_process.parent() + + print("Reaping test: stage_1 is pid{0}, init is pid{1}".format(this_process.pid, init_process.pid)) + + # The only child PID that should persist is this one. + expected_pids = [this_process.pid] print("Expecting pids to remain: {0}".format(", ".join(str(pid) for pid in expected_pids))) while 1: - pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] - print("Has pids: {0}".format(", ".join(pids))) - if set(int(pid) for pid in pids) == set(expected_pids): + pids = [p.pid for p in init_process.children(recursive=True)] + print("Has pids: {0}".format(", ".join(str(pid) for pid in pids))) + if set(pids) == set(expected_pids): break time.sleep(1) +if __name__ == "__main__": + main() diff --git a/test/reaping/stage_2.py b/test/reaping/stage_2.py index f4cc17d..1156afc 100755 --- a/test/reaping/stage_2.py +++ b/test/reaping/stage_2.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python from __future__ import print_function import subprocess import os diff --git a/test/run_inner_tests.py b/test/run_inner_tests.py new file mode 100755 index 0000000..5e25a6b --- /dev/null +++ b/test/run_inner_tests.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +#coding:utf-8 +import os +import sys +import signal +import subprocess + + +def main(): + src = os.environ["SOURCE_DIR"] + build = os.environ["BUILD_DIR"] + + proxy = os.path.join(src, "test", "subreaper-proxy.py") + tini = os.path.join(build, "tini") + + # Run the reaping test + print "Running reaping test" + p = subprocess.Popen([proxy, tini, "--", os.path.join(src, "test", "reaping", "stage_1.py")]) + ret = p.wait() + assert ret == 0, "Reaping test failed!" + + # Run the signals test + for signame in "SIGINT", "SIGTERM": + print "running signal test for: {0}".format(signame) + p = subprocess.Popen([proxy, tini, "--", os.path.join(src, "test", "signals", "test.py")]) + sig = getattr(signal, signame) + p.send_signal(sig) + ret = p.wait() + assert ret == - sig, "Signals test failed!" + + +if __name__ == "__main__": + main() diff --git a/test/test.py b/test/run_outer_tests.py old mode 100644 new mode 100755 similarity index 100% rename from test/test.py rename to test/run_outer_tests.py diff --git a/test/signals/test.py b/test/signals/test.py index ad320ef..bf225f1 100755 --- a/test/signals/test.py +++ b/test/signals/test.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python import time if __name__ == "__main__": diff --git a/test/subreaper-proxy.py b/test/subreaper-proxy.py new file mode 100755 index 0000000..a125a44 --- /dev/null +++ b/test/subreaper-proxy.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +#coding:utf-8 +import os +import sys + +import prctl + + +def main(): + args = sys.argv[1:] + + print "subreaper-proxy: running '%s'" % (" ".join(args)) + + prctl.set_child_subreaper(1) + os.execv(args[0], args) + + +if __name__ == '__main__': + main() diff --git a/tpl/travis.yml.tpl b/tpl/travis.yml.tpl index 2af079c..57df6e5 100644 --- a/tpl/travis.yml.tpl +++ b/tpl/travis.yml.tpl @@ -18,6 +18,10 @@ addons: - git - gdb - valgrind + - python-dev + - libcap-dev + - python-pip + - python-virtualenv script: ./ci/run_build.sh