mirror of
https://github.com/peterbourgon/runsvinit.git
synced 2024-12-16 14:57:04 +00:00
First draft
This commit is contained in:
parent
33c958bc9d
commit
4e6968fe8f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
reap
|
||||||
|
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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/*
|
||||||
|
|
||||||
|
ADD reap /
|
||||||
|
|
||||||
|
ADD foo /
|
||||||
|
RUN mkdir -p /etc/service/foo
|
||||||
|
ADD run-foo /etc/service/foo/run
|
||||||
|
|
||||||
|
ADD bar /
|
||||||
|
RUN mkdir -p /etc/service/bar
|
||||||
|
ADD run-bar /etc/service/bar/run
|
||||||
|
|
||||||
|
ENTRYPOINT ["/reap"]
|
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# reap
|
||||||
|
|
||||||
|
If you have a Docker container that's a collection of runit-supervised daemons,
|
||||||
|
this process is suitable for use as the ENTRYPOINT.
|
||||||
|
|
||||||
|
```Docker
|
||||||
|
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/*
|
||||||
|
|
||||||
|
ADD foo /
|
||||||
|
RUN mkdir -p /etc/service/foo
|
||||||
|
ADD run-foo /etc/service/foo/run
|
||||||
|
|
||||||
|
ADD bar /
|
||||||
|
RUN mkdir -p /etc/service/bar
|
||||||
|
ADD run-bar /etc/service/bar/run
|
||||||
|
|
||||||
|
ADD reap /
|
||||||
|
ENTRYPOINT ["/reap"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why not just exec runsvdir?**
|
||||||
|
|
||||||
|
`docker stop` issues SIGTERM (or, in a future version of Docker, perhaps another custom signal)
|
||||||
|
but if runsvdir receives a signal,
|
||||||
|
it doesn't wait for its supervised processes to exit before returning.
|
||||||
|
If you don't care about graceful shutdown of your daemons, no problem, you don't need this tool.
|
||||||
|
|
||||||
|
**Why not wrap runsvdir in a simple shell script?**
|
||||||
|
|
||||||
|
This works great:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
sv_stop() {
|
||||||
|
for s in $(ls -d /etc/service/*)
|
||||||
|
do
|
||||||
|
/sbin/sv stop $s
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
trap "sv_stop; exit" SIGTERM
|
||||||
|
/sbin/runsvdir /etc/service &
|
||||||
|
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?**
|
||||||
|
|
||||||
|
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?**
|
||||||
|
|
||||||
|
Basically, yes.
|
||||||
|
|
15
bar
Executable file
15
bar
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
echo "got signal"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
trap handle SIGINT
|
||||||
|
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
echo bar: `date`
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
17
foo
Executable file
17
foo
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
echo "got signal"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
trap "handle" SIGINT
|
||||||
|
|
||||||
|
for i in `seq 1 5`
|
||||||
|
do
|
||||||
|
echo foo: $i
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo foo: terminating!
|
||||||
|
|
135
main.go
Normal file
135
main.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const etcService = "/etc/service"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
runsvdir, err := exec.LookPath("runsvdir")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sv, err := exec.LookPath("sv")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, err := os.Stat(etcService); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else if !fi.IsDir() {
|
||||||
|
log.Fatalf("%s is not a directory", etcService)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pid := os.Getpid(); pid != 1 {
|
||||||
|
log.Printf("warning: I'm not PID 1, I'm PID %d", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
go reapAll()
|
||||||
|
|
||||||
|
supervisor := cmd(runsvdir, etcService)
|
||||||
|
if err := supervisor.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s started", runsvdir)
|
||||||
|
|
||||||
|
go gracefulShutdown(sv, supervisor.Process)
|
||||||
|
|
||||||
|
if err := supervisor.Wait(); err != nil {
|
||||||
|
log.Printf("%s exited with error: %v", runsvdir, err)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
func reapOne() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
log.Printf("reaped child process %d (%+v)", pid, ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
type signaler interface {
|
||||||
|
Signal(os.Signal) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func gracefulShutdown(sv string, s signaler) {
|
||||||
|
c := make(chan os.Signal)
|
||||||
|
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
log.Printf("received %s", <-c)
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(filepath.Join(etcService, "*"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("when shutting down services: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopped []string
|
||||||
|
for _, match := range matches {
|
||||||
|
fi, err := os.Stat(match)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s: %v", match, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
log.Printf("%s: not a directory", match)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
service := filepath.Base(match)
|
||||||
|
stop := cmd(sv, "stop", service)
|
||||||
|
if err := stop.Run(); err != nil {
|
||||||
|
log.Printf("%s: %v", strings.Join(stop.Args, " "), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stopped = append(stopped, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("stopped %d: %s", len(stopped), strings.Join(stopped, ", "))
|
||||||
|
log.Printf("stopping supervisor...")
|
||||||
|
if err := s.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
log.Printf("graceful SIGTERM handler exiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmd(path string, args ...string) *exec.Cmd {
|
||||||
|
return &exec.Cmd{
|
||||||
|
Path: path,
|
||||||
|
Args: append([]string{path}, args...),
|
||||||
|
Env: os.Environ(),
|
||||||
|
Stdin: os.Stdin,
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user