package main import ( "encoding/json" "errors" "flag" "fmt" "io" "os" "os/exec" "os/signal" "sync" "syscall" "time" "golang.org/x/sys/unix" ) var ch chan struct{} func init() { ch = make(chan struct{}) } type Service struct { CMD string `json:"cmd"` Args []string `json:"args"` PWD string `json:"pwd"` cmd *exec.Cmd } type Config struct { Services []*Service `json:"services"` } func printErr(err error) { fmt.Fprintf(os.Stderr, "[qini] Err: %s\n", err.Error()) } // func printInfo(info string) { // fmt.Fprintf(os.Stderr, "[qini] Inf: %s\n", info) // } var verbose bool func debug(s string, a ...any) { if verbose { fmt.Fprintf(os.Stderr, "[qini] debug: %s\n", fmt.Sprintf(s, a...)) } } func stopAllServices(services []*Service) { for _, s := range services { if s.cmd.Process == nil { continue } debug("send SIGINT to [%s]", s.CMD) s.cmd.Process.Signal(syscall.SIGINT) go func(s *Service) { time.Sleep(15 * time.Second) if s.cmd.Process != nil && s.cmd.ProcessState == nil { debug("send SIGKILL to [%s]", s.CMD) s.cmd.Process.Kill() } }(s) } loop: for { time.Sleep(time.Second) for _, s := range services { if s.cmd.Process != nil && s.cmd.ProcessState == nil { continue loop } } os.Exit(1) } } func runServices(services []*Service) error { if len(services) == 0 { return errors.New("no services in config") } var ( r *io.PipeReader w *io.PipeWriter ) for i, s := range services { s.cmd = exec.Command(s.CMD, s.Args...) s.cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: false, } if s.PWD != "" { s.cmd.Dir = s.PWD } if r != nil { s.cmd.Stdin = r } if i+1 < len(services) { r, w = io.Pipe() s.cmd.Stdout = w } else { s.cmd.Stdout = os.Stdout } debug("run service [%s]", s.CMD) err := s.cmd.Start() if err != nil { printErr(err) } go func(s *Service, w *io.PipeWriter) { s.cmd.Wait() w.Close() debug("service done [%s]", s.CMD) }(s, w) } return nil } func loadConfig(cfg *Config, configFile string) error { f, err := os.Open(configFile) if err != nil { return err } defer f.Close() return json.NewDecoder(f).Decode(cfg) } func forwardSignals(services []*Service) { signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT} sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, signals...) for sig := range sigCh { for _, s := range services { if s.cmd.Process == nil { continue } syscall.Kill(s.cmd.Process.Pid, (sig).(syscall.Signal)) } } } func forwardSignals2(services []*Service) { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGCHLD) var once sync.Once for range sigCh { debug("chld") for _, c := range services { debug("%v", c.cmd.ProcessState) if c.cmd.ProcessState != nil { once.Do(func() { debug("stop all services") stopAllServices(services) }) } } } } func main() { verbose = false err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) if err != nil { printErr(err) } var configFile string flag.StringVar(&configFile, "c", "./config.json", "JSON config file") flag.BoolVar(&verbose, "v", false, "Show debug output") flag.Parse() var cfg Config err = loadConfig(&cfg, configFile) if err != nil { printErr(err) os.Exit(1) } go forwardSignals(cfg.Services) go forwardSignals2(cfg.Services) runServices(cfg.Services) //os.Exit(exitCode) <-ch }