once/docker_wrapper.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"reflect"
"syscall"
"github.com/goaux/decowriter"
"golang.org/x/sys/unix"
)
type ChildResult struct {
Child *Child
ProcessState *os.ProcessState
Err error
}
type Child struct {
cmd *exec.Cmd
resultChannel chan ChildResult
}
func NewChild(commandline []string, stdout io.Writer, stderr io.Writer) *Child {
cmd := exec.Command(commandline[0], commandline[1:]...)
// By setting Stdout and Stderr,
// Go will copy the process stdout and the process stderr to Stdout and Stderr for us.
cmd.Stdout = stdout
cmd.Stderr = stderr
resultChannel := make(chan ChildResult)
return &Child{
cmd: cmd,
resultChannel: resultChannel,
}
}
func (c *Child) String() string {
return c.cmd.Path
}
func (c *Child) Start() error {
err := c.cmd.Start()
if err != nil {
return err
}
go func() {
var exitError *exec.ExitError
err := c.cmd.Wait()
if errors.As(err, &exitError) {
c.resultChannel <- ChildResult{
Child: c,
ProcessState: exitError.ProcessState,
}
} else if err != nil {
c.resultChannel <- ChildResult{
Child: c,
Err: err,
}
} else {
c.resultChannel <- ChildResult{
Child: c,
ProcessState: c.cmd.ProcessState,
}
}
}()
return nil
}
func (c *Child) Chan() <-chan ChildResult {
return c.resultChannel
}
func (c *Child) Signal(signal os.Signal) error {
return c.cmd.Process.Signal(signal)
}
type ParentState int
const (
ParentStateRunning = 0
ParentStateReaping = 1
)
func SignalName(sig os.Signal) string {
if syscallSignal, ok := sig.(syscall.Signal); ok {
return unix.SignalName(syscallSignal)
}
return sig.String()
}
func main() {
parentStdout := decowriter.New(os.Stdout, []byte("docker_wrapper | "), []byte(""))
//parentStderr := decowriter.New(os.Stderr, []byte("docker_wrapper | "), []byte(""))
parentState := ParentStateRunning
// https://pkg.go.dev/os/signal@go1.23.6#hdr-Default_behavior_of_signals_in_Go_programs
signalsForReaping := []os.Signal{
// For some unknown reason, the commented signals are not available on Linux.
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
syscall.SIGILL,
syscall.SIGTRAP,
syscall.SIGABRT,
// syscall.SIGSTKFLT,
// syscall.SIGEMT,
syscall.SIGSYS,
}
// signal.Notify says we must use buffered channel.
// It also says a buffer of size 1 is usually enough.
signalChan := make(chan os.Signal, 1)
// We want to forward all signal to child, so listen for all signals.
signal.Notify(signalChan)
selectCases := []reflect.SelectCase{
// case sig := <-signalChan:
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(signalChan)},
}
postgres := NewChild(
[]string{"postgres"},
decowriter.New(os.Stdout, []byte("postgres | "), []byte("")),
decowriter.New(os.Stderr, []byte("postgres | "), []byte("")),
)
redis := NewChild(
[]string{"redis-server", "/etc/redis/redis.conf"},
decowriter.New(os.Stdout, []byte("redis | "), []byte("")),
decowriter.New(os.Stderr, []byte("redis | "), []byte("")),
)
nginx := NewChild(
[]string{"nginx"},
decowriter.New(os.Stdout, []byte("nginx | "), []byte("")),
decowriter.New(os.Stderr, []byte("nginx | "), []byte("")),
)
certbot_daemon := NewChild(
[]string{"docker-certbot.py"},
decowriter.New(os.Stdout, []byte("docker-certbot | "), []byte("")),
decowriter.New(os.Stderr, []byte("docker-certbot | "), []byte("")),
)
certbot_once := NewChild(
[]string{"docker-certbot.py", "--block-on-getting-tls-certificates"},
decowriter.New(os.Stdout, []byte("docker-certbot | "), []byte("")),
decowriter.New(os.Stderr, []byte("docker-certbot | "), []byte("")),
)
minio := NewChild(
[]string{"minio", "server", os.Getenv("AUTHGEARONCE_MINIO_DATA_DIRECTORY"), "--console-address", ":9001"},
decowriter.New(os.Stdout, []byte("minio | "), []byte("")),
decowriter.New(os.Stderr, []byte("minio | "), []byte("")),
)
authgeardeno := NewChild(
[]string{"authgear-deno"},
decowriter.New(os.Stdout, []byte("authgear-deno | "), []byte("")),
decowriter.New(os.Stderr, []byte("authgear-deno | "), []byte("")),
)
authgear := NewChild(
[]string{"authgear", "start"},
decowriter.New(os.Stdout, []byte("authgear | "), []byte("")),
decowriter.New(os.Stderr, []byte("authgear | "), []byte("")),
)
authgearportal := NewChild(
[]string{"authgear-portal", "start"},
decowriter.New(os.Stdout, []byte("authgear-portal | "), []byte("")),
decowriter.New(os.Stderr, []byte("authgear-portal | "), []byte("")),
)
authgearimages := NewChild(
[]string{"authgear", "images", "start"},
decowriter.New(os.Stdout, []byte("authgear-images | "), []byte("")),
decowriter.New(os.Stderr, []byte("authgear-images | "), []byte("")),
)
childResults := []ChildResult{}
startedChildren := []*Child{}
start := func(child *Child) {
err := child.Start()
if err != nil {
childResults = append(childResults, ChildResult{
Child: child,
Err: err,
})
} else {
startedChildren = append(startedChildren, child)
// case childResult := <- child.Chan()
selectCases = append(selectCases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(child.Chan())})
}
}
startReap := func(sig os.Signal) {
if parentState == ParentStateReaping {
return
}
parentState = ParentStateReaping
for _, child := range startedChildren {
fmt.Fprintf(parentStdout, "sending %v to %v\n", SignalName(sig), child)
child.Signal(sig)
}
}
if len(os.Args) >= 2 && os.Args[1] == "--block-on-getting-tls-certificates" {
start(nginx)
start(certbot_once)
} else {
start(postgres)
start(redis)
start(nginx)
start(certbot_daemon)
start(minio)
start(authgeardeno)
start(authgear)
start(authgearportal)
start(authgearimages)
}
// Some child failed to start, trigger reap now.
if len(childResults) > 0 {
startReap(syscall.SIGTERM)
}
for len(childResults) < len(startedChildren) {
// This is a dynamic form of the select statement.
chosenIdx, recv, _ := reflect.Select(selectCases)
switch {
// case sig := <-signalChan:
case chosenIdx == 0:
sig := recv.Interface().(os.Signal)
fmt.Fprintf(parentStdout, "received signal: %v\n", SignalName(sig))
for _, signalForReaping := range signalsForReaping {
if signalForReaping == sig {
fmt.Fprintf(parentStdout, "start reaping because signal is %v\n", SignalName(sig))
startReap(sig)
}
}
// case childResult := <-child.Chan()
default:
childResult := recv.Interface().(ChildResult)
childResults = append(childResults, childResult)
// We are supposed to be running but one of the child terminated.
// Start reaping.
if parentState == ParentStateRunning {
fmt.Fprintf(parentStdout, "start reaping because %v has terminated\n", childResult.Child)
startReap(syscall.SIGTERM)
}
}
}
// parentExitCode is normally 0.
// If one of the child exited with non-zero, then the exit code of the parent is non-zero.
parentExitCode := 0
for _, childResult := range childResults {
if childResult.Err != nil {
fmt.Fprintf(parentStdout, "%v resulted in err: %v\n", childResult.Child, childResult.Err)
} else if childResult.ProcessState != nil {
childExitCode := childResult.ProcessState.ExitCode()
fmt.Fprintf(parentStdout, "%v exited with %v\n", childResult.Child, childExitCode)
if childExitCode != 0 {
parentExitCode = childExitCode
}
}
}
fmt.Fprintf(parentStdout, "exiting with %v\n", parentExitCode)
os.Exit(parentExitCode)
}