diff --git a/.gitignore b/.gitignore index 552a1b1..02812de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ runsvinit runsvinit-*-* examples/runsvinit-linux-amd64* +zombietest/zombie +zombietest/runsvinit +*.uptodate # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..97109ef --- /dev/null +++ b/circle.yml @@ -0,0 +1,8 @@ +machine: + services: + - docker + +test: + override: + - go test -v + - make CIRCLECI=true SUDO=sudo RM= -C zombietest diff --git a/main.go b/main.go index 93b8af1..82f1dcc 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "log" "os" "os/exec" @@ -13,6 +14,9 @@ import ( const etcService = "/etc/service" func main() { + reap := flag.Bool("reap", true, "reap orphan children") + flag.Parse() + log.SetFlags(0) runsvdir, err := exec.LookPath("runsvdir") @@ -35,7 +39,12 @@ func main() { log.Printf("warning: I'm not PID 1, I'm PID %d", pid) } - go reapAll() + if *reap { + log.Print("reaping zombies") + go reapLoop() + } else { + log.Print("NOT reaping zombies") + } supervisor := cmd(runsvdir, etcService) if err := supervisor.Start(); err != nil { @@ -53,31 +62,33 @@ func main() { } } -func reapAll() { +// From https://github.com/ramr/go-reaper/blob/master/reaper.go +func reapLoop() { c := make(chan os.Signal) signal.Notify(c, syscall.SIGCHLD) for range c { - go reapOne() + reapChildren() } } -// From https://github.com/ramr/go-reaper/blob/master/reaper.go -func reapOne() { - var ( - ws syscall.WaitStatus - pid int - err error - ) +func reapChildren() { for { - pid, err = syscall.Wait4(-1, &ws, 0, nil) - if err != syscall.EINTR { - break + var ( + ws syscall.WaitStatus + pid int + err error + ) + for { + pid, err = syscall.Wait4(-1, &ws, 0, nil) + if err != syscall.EINTR { + break + } } + if err == syscall.ECHILD { + return // done + } + log.Printf("reaped child process %d (%+v)", pid, ws) } - if err == syscall.ECHILD { - return - } - log.Printf("reaped child process %d (%+v)", pid, ws) } type signaler interface { diff --git a/zombietest/Dockerfile b/zombietest/Dockerfile new file mode 100644 index 0000000..3012c1a --- /dev/null +++ b/zombietest/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:latest +RUN echo "http://dl-4.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories && \ + apk add --update runit && \ + rm -rf /var/cache/apk/* + +COPY zombie / +RUN mkdir -p /etc/service/zombie +COPY run-zombie /etc/service/zombie/run + +COPY /runsvinit / + diff --git a/zombietest/Makefile b/zombietest/Makefile new file mode 100644 index 0000000..c1d9b19 --- /dev/null +++ b/zombietest/Makefile @@ -0,0 +1,27 @@ +GO?=go +SUDO?= +RM?=--rm + +.PHONY: test clean + +test: .test.uptodate + ./test.bash + +.test.uptodate: runsvinit zombie run-zombie Dockerfile + $(SUDO) docker build -t zombietest . + touch $@ + +runsvinit: ../*.go + env GOOS=linux GOARCH=amd64 $(GO) build -o $@ github.com/peterbourgon/runsvinit + +zombie: .build.uptodate + $(SUDO) docker run $(RM) -v $(shell pwd):/mount zombietest-build cc -Wall -Werror -o /mount/zombie /zombie.c + +.build.uptodate: build/zombie.c build/Dockerfile + $(SUDO) docker build -t zombietest-build build/ + touch $@ + +clean: + rm -rf .test.uptodate .build.uptodate runsvinit zombie + $(SUDO) docker stop zombietest zombietest-build >/dev/null 2>&1 || true + $(SUDO) docker rm zombietest zombietest-build >/dev/null 2>&1 || true diff --git a/zombietest/README.md b/zombietest/README.md new file mode 100644 index 0000000..c2a1067 --- /dev/null +++ b/zombietest/README.md @@ -0,0 +1,24 @@ +# zombietest + +This directory contains an integration test to prove runsvinit is actually +reaping zombies. `make` builds and executes the test as follows: + +1. We produce a linux/amd64 runsvinit binary by setting GOOS/GOARCH and + invoking the Go compiler. Requires Go 1.5, or Go 1.4 built with the + appropriate cross-compile options. + +2. The build/zombie.c program spawns five zombies and exits. We compile it for + linux/amd64 via a zombietest-build container. We do this so `make` works + from a Mac. This requires a working Docker installation. + +3. Once we have linux/amd64 runsvinit and zombie binaries, we produce a + zombietest container via the Dockerfile. That container contains a single + runit service, /etc/service/zombie, which supervises the zombie binary. We + provide no default ENTRYPOINT, so we can supply it at runtime. + +4. Once the zombietest container is built, we invoke the test.bash script. + That launches a version of the container with runsvinit set to NOT reap + zombies, and after 1 second, verifies that zombies exist. Then, it launches + a version of the container with runsvinit set to reap zombies, and after 1 + second, verifies that no zombies exist. + diff --git a/zombietest/build/Dockerfile b/zombietest/build/Dockerfile new file mode 100644 index 0000000..2828f5e --- /dev/null +++ b/zombietest/build/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:latest +RUN apk add --update gcc musl-dev && rm -rf /var/cache/apk/* +COPY zombie.c / diff --git a/zombietest/build/zombie.c b/zombietest/build/zombie.c new file mode 100644 index 0000000..0859123 --- /dev/null +++ b/zombietest/build/zombie.c @@ -0,0 +1,18 @@ +#include +#include +#include + +int main() { + pid_t pid; + int i; + for (i = 0; i<5; i++) { + pid = fork(); + if (pid > 0) { + printf("Zombie #%d born\n", i + 1); + } else { + printf("Brains...\n"); + exit(0); + } + } + return 0; +} diff --git a/zombietest/run-zombie b/zombietest/run-zombie new file mode 100755 index 0000000..198fbc2 --- /dev/null +++ b/zombietest/run-zombie @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /zombie diff --git a/zombietest/test.bash b/zombietest/test.bash new file mode 100755 index 0000000..f6339d8 --- /dev/null +++ b/zombietest/test.bash @@ -0,0 +1,47 @@ +#!/bin/bash + +function zombies() { + if [ -z "$CIRCLECI" ] + then + docker exec $C ps -o pid,stat | grep Z | wc -l + else + # https://circleci.com/docs/docker#docker-exec + sudo lxc-attach -n "$(docker inspect --format '{{.Id}}' $C)" -- sh -c "ps -o pid,stat | grep Z | wc -l" + fi +} + +function stop_rm() { + docker stop $1 + docker rm $1 +} + +SLEEP=1 +RC=0 + +C=$(docker run -d zombietest /runsvinit -reap=false) +sleep $SLEEP +NOREAP=$(zombies) +echo -n without reaping, we have $NOREAP zombies... +if [ "$NOREAP" -le "0" ] +then + echo " FAIL" + RC=1 +else + echo " good" +fi +stop_rm $C + +C=$(docker run -d zombietest /runsvinit) +sleep $SLEEP +YESREAP=$(zombies) +echo -n with reaping, we have $YESREAP zombies... +if [ "$YESREAP" -gt "0" ] +then + echo " FAIL" + RC=1 +else + echo " good" +fi +stop_rm $C + +exit $RC