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) }