Compare commits

..

No commits in common. "master" and "v2.0.0" have entirely different histories.

12 changed files with 64 additions and 244 deletions

3
.gitignore vendored
View File

@ -1,9 +1,6 @@
runsvinit runsvinit
runsvinit-*-* runsvinit-*-*
examples/runsvinit-linux-amd64* examples/runsvinit-linux-amd64*
zombietest/zombie
zombietest/runsvinit
*.uptodate
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o *.o

View File

@ -1,27 +1,19 @@
# runsvinit [![Circle CI](https://circleci.com/gh/peterbourgon/runsvinit.svg?style=svg)](https://circleci.com/gh/peterbourgon/runsvinit) # runsvinit
If you have a Docker container that's a collection of runit-supervised daemons, this process is suitable for use as the ENTRYPOINT. If you have a Docker container that's a collection of runit-supervised daemons,
this process is suitable for use as the ENTRYPOINT.
See [the example](https://github.com/peterbourgon/runsvinit/tree/master/example). See [the example](https://github.com/peterbourgon/runsvinit/tree/master/example).
**Why not use runit(8) directly?** **Why not just exec runsvdir?**
[runit(8)](http://smarden.org/runit/runit.8.html) is designed to be used as process 1. `docker stop` issues SIGTERM (or, in a future version of Docker, perhaps another custom signal)
And, if you provide an `/etc/service/ctrlaltdel` script, it will be executed when runit receives the INT signal. but if runsvdir receives a signal,
So, we could use that hook to gracefully terminate our services. it doesn't wait for its supervised processes to exit before returning.
But Docker only sends TERM on `docker stop`. If you don't care about graceful shutdown of your daemons, no problem, you don't need this tool.
Note that the container stop signal [will become configurable](https://github.com/docker/docker/pull/15307) in Docker 1.9. **Why not wrap runsvdir in a simple shell script?**
Once Docker 1.9 ships, this utility will be obsolete.
**Why not just exec runsvdir(8) directly?** This works great:
If [runsvdir(8)](http://smarden.org/runit/runsvdir.8.html) receives a signal, it doesn't wait for its supervised processes to exit before returning.
**Why not wrap runsvdir(8) in a simple shell script?**
Process 1 has the additional responsibility of [reaping orphaned child processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/).
To the best of my knowledge, there is no sane way to do this with a shell script.
Otherwise, indeed, this would work great:
```sh ```sh
#!/bin/sh #!/bin/sh
@ -38,10 +30,14 @@ trap "sv_stop; exit" SIGTERM
wait wait
``` ```
...except it doesn't [reap orphaned child processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
and is therefore unsuitable for being PID 1.
**Why not use my_init from [phusion/baseimage-docker](https://github.com/phusion/baseimage-docker)?** **Why not use my_init from [phusion/baseimage-docker](https://github.com/phusion/baseimage-docker)?**
my_init depends on Python 3, which might be a big dependency for such a small responsibility. That works great — if you're willing to add python3 to your Docker images :)
**So this is just a stripped-down my_init in Go?** **So this is just a stripped-down my_init in Go?**
Basically, yes. Basically, yes.

View File

@ -1,8 +0,0 @@
machine:
services:
- docker
test:
override:
- go test -v
- make CIRCLECI=true SUDO=sudo RMCONTAINER= -C zombietest

View File

@ -2,9 +2,9 @@
docker: runsvinit docker: runsvinit
docker build -t runsvinit-example . docker build -t runsvinit-example .
runsvinit: $GOPATH/bin/runsvinit runsvinit: runsvinit-linux-amd64.tgz
cp $< $@ tar zxf $<
$GOPATH/bin/runsvinit: runsvinit-linux-amd64.tgz:
go get -u github.com/peterbourgon/runsvinit wget --quiet https://github.com/peterbourgon/runsvinit/releases/download/v2.0.0/runsvinit-linux-amd64.tgz

121
main.go
View File

@ -1,109 +1,83 @@
package main package main
import ( import (
"flag"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"syscall" "syscall"
) )
const etcService = "/etc/service" const etcService = "/etc/service"
var (
debugf = log.Printf
info = log.Print
infof = log.Printf
fatal = log.Fatal
fatalf = log.Fatalf
)
func main() { func main() {
var (
reap = flag.Bool("reap", true, "reap orphan children")
debug = flag.Bool("debug", false, "log debug information")
)
flag.Parse()
log.SetFlags(0) log.SetFlags(0)
if !*debug {
debugf = func(string, ...interface{}) {}
}
runsvdir, err := exec.LookPath("runsvdir") runsvdir, err := exec.LookPath("runsvdir")
if err != nil { if err != nil {
fatal(err) log.Fatal(err)
} }
sv, err := exec.LookPath("sv") sv, err := exec.LookPath("sv")
if err != nil { if err != nil {
fatal(err) log.Fatal(err)
} }
if fi, err := os.Stat(etcService); err != nil { if fi, err := os.Stat(etcService); err != nil {
fatal(err) log.Fatal(err)
} else if !fi.IsDir() { } else if !fi.IsDir() {
fatalf("%s is not a directory", etcService) log.Fatalf("%s is not a directory", etcService)
} }
if pid := os.Getpid(); pid != 1 { if pid := os.Getpid(); pid != 1 {
debugf("warning: I'm not PID 1, I'm PID %d", pid) log.Printf("warning: I'm not PID 1, I'm PID %d", pid)
} }
if *reap { go reapAll()
go reapLoop()
} else {
infof("warning: NOT reaping zombies")
}
supervisor := cmd(runsvdir, etcService) supervisor := cmd(runsvdir, etcService)
if err := supervisor.Start(); err != nil { if err := supervisor.Start(); err != nil {
fatal(err) log.Fatal(err)
} }
debugf("%s started", runsvdir) log.Printf("%s started", runsvdir)
go shutdown(sv, supervisor.Process) go shutdown(sv, supervisor.Process)
if err := supervisor.Wait(); err != nil { if err := supervisor.Wait(); err != nil {
infof("%s exited with error: %v", runsvdir, err) log.Printf("%s exited with error: %v", runsvdir, err)
} else { } else {
debugf("%s exited cleanly", runsvdir) log.Printf("%s exited cleanly", runsvdir)
}
}
func reapAll() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGCHLD)
for range c {
go reapOne()
} }
} }
// From https://github.com/ramr/go-reaper/blob/master/reaper.go // From https://github.com/ramr/go-reaper/blob/master/reaper.go
func reapLoop() { func reapOne() {
c := make(chan os.Signal) var (
signal.Notify(c, syscall.SIGCHLD) ws syscall.WaitStatus
for range c { pid int
reapChildren() err error
} )
}
func reapChildren() {
for { for {
var ( pid, err = syscall.Wait4(-1, &ws, 0, nil)
ws syscall.WaitStatus if err != syscall.EINTR {
pid int break
err error
)
for {
pid, err = syscall.Wait4(-1, &ws, 0, nil)
if err != syscall.EINTR {
break
}
} }
if err == syscall.ECHILD {
return // done
}
infof("reaped child process %d (%+v)", pid, ws)
} }
if err == syscall.ECHILD {
return
}
log.Printf("reaped child process %d (%+v)", pid, ws)
} }
type signaler interface { type signaler interface {
@ -114,45 +88,40 @@ func shutdown(sv string, s signaler) {
c := make(chan os.Signal) c := make(chan os.Signal)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
sig := <-c sig := <-c
debugf("received %s", sig) log.Printf("received %s", sig)
matches, err := filepath.Glob(filepath.Join(etcService, "*")) matches, err := filepath.Glob(filepath.Join(etcService, "*"))
if err != nil { if err != nil {
infof("when shutting down services: %v", err) log.Printf("when shutting down services: %v", err)
return return
} }
var wg sync.WaitGroup var stopped []string
for _, match := range matches { for _, match := range matches {
fi, err := os.Stat(match) fi, err := os.Stat(match)
if err != nil { if err != nil {
infof("%s: %v", match, err) log.Printf("%s: %v", match, err)
continue continue
} }
if !fi.IsDir() { if !fi.IsDir() {
infof("%s: not a directory", match) log.Printf("%s: not a directory", match)
continue continue
} }
service := filepath.Base(match) service := filepath.Base(match)
stop := cmd(sv, "stop", service) stop := cmd(sv, "stop", service)
debugf("stopping %s...", service) if err := stop.Run(); err != nil {
wg.Add(1) log.Printf("%s: %v", strings.Join(stop.Args, " "), err)
go func() { continue
defer wg.Done() }
if err := stop.Run(); err != nil { stopped = append(stopped, service)
infof("%s: %v", strings.Join(stop.Args, " "), err)
} else {
debugf("stopped %s", service)
}
}()
} }
wg.Wait()
debugf("stopping supervisor with signal %s...", sig) log.Printf("stopped %d: %s", len(stopped), strings.Join(stopped, ", "))
log.Printf("stopping supervisor with signal %s...", sig)
if err := s.Signal(sig); err != nil { if err := s.Signal(sig); err != nil {
info(err) log.Print(err)
} }
debugf("shutdown handler exiting") log.Printf("shutdown handler exiting")
} }
func cmd(path string, args ...string) *exec.Cmd { func cmd(path string, args ...string) *exec.Cmd {

View File

@ -1,11 +0,0 @@
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 /

View File

@ -1,27 +0,0 @@
GO ?= go
SUDO ?=
RMCONTAINER ?= --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 $(RMCONTAINER) -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

View File

@ -1,24 +0,0 @@
# 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.

View File

@ -1,3 +0,0 @@
FROM alpine:latest
RUN apk add --update gcc musl-dev && rm -rf /var/cache/apk/*
COPY zombie.c /

View File

@ -1,18 +0,0 @@
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
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;
}

View File

@ -1,3 +0,0 @@
#!/bin/sh
exec /zombie

View File

@ -1,48 +0,0 @@
#!/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 >/dev/null
#docker logs $1
docker rm $1 >/dev/null
}
SLEEP=1
RC=0
C=$(docker run -d zombietest /runsvinit -reap=false -debug)
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 -debug)
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