Compare commits
No commits in common. "master" and "v2.0.0" have entirely different histories.
|
@ -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
|
||||||
|
|
34
README.md
34
README.md
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
machine:
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
test:
|
|
||||||
override:
|
|
||||||
- go test -v
|
|
||||||
- make CIRCLECI=true SUDO=sudo RMCONTAINER= -C zombietest
|
|
|
@ -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
121
main.go
|
@ -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 {
|
||||||
|
|
|
@ -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 /
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk add --update gcc musl-dev && rm -rf /var/cache/apk/*
|
|
||||||
COPY zombie.c /
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
exec /zombie
|
|
|
@ -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
|
|
Loading…
Reference in New Issue