package main import ( "encoding/json" "errors" "flag" "fmt" "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()) } 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 || s.cmd.ProcessState != 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) } } func runServices(services []*Service) (int, error) { if len(services) == 0 { return 0, errors.New("no services in config") } var ( r *os.File w *os.File once sync.Once exitCode int wg sync.WaitGroup err error ) for i, s := range services { s.cmd = exec.Command(s.CMD, s.Args...) s.cmd.SysProcAttr = &syscall.SysProcAttr{ //Setsid: true, } if s.PWD != "" { s.cmd.Dir = s.PWD } if r != nil { s.cmd.Stdin = r } else { s.cmd.Stdin = os.Stdin } if i+1 < len(services) { r, w, err = os.Pipe() if err != nil { return 0, err } 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) } wg.Add(1) go func(s *Service, w *os.File) { defer wg.Done() s.cmd.Wait() w.Close() debug("service done [%s]", s.CMD) exitCode = s.cmd.ProcessState.ExitCode() once.Do(func() { debug("stop all services") stopAllServices(services) }) }(s, w) } wg.Wait() return exitCode, 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.SIGCHLD, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGSEGV} sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, signals...) for sig := range sigCh { switch sig { case syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT: for _, s := range services { if s.cmd.Process == nil { continue } syscall.Kill(s.cmd.Process.Pid, (sig).(syscall.Signal)) } case syscall.SIGCHLD: debug("chld") } } } func main() { var ( configFile string cfg Config err error ) err = unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) if err != nil { printErr(err) } flag.StringVar(&configFile, "c", "./config.json", "JSON config file") flag.BoolVar(&verbose, "v", false, "Show debug output") flag.Parse() err = loadConfig(&cfg, configFile) if err != nil { printErr(err) os.Exit(1) } go forwardSignals(cfg.Services) exitCode, err := runServices(cfg.Services) if err != nil { printErr(err) os.Exit(123) } os.Exit(exitCode) }