package main import ( "encoding/json" "flag" "fmt" "io" "os" "os/exec" "os/signal" "sync" "syscall" "time" "golang.org/x/sys/unix" ) 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) } } func runServices(services []*Service) int { if len(services) == 0 { printInfo("no services in config") return 1 } var ( r *io.PipeReader w *io.PipeWriter lock sync.Mutex once sync.Once wg sync.WaitGroup exitCode int ) lock.Lock() 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 } wg.Add(1) go func(w *io.PipeWriter) { defer wg.Done() defer func() { if w != nil { w.Close() } once.Do(func() { lock.Lock() defer lock.Unlock() debug("stop all services") stopAllServices(services) exitCode = s.cmd.ProcessState.ExitCode() }) }() debug("run service [%s]", s.CMD) err := s.cmd.Run() if err != nil { printErr(err) } debug("service [%s] done, err: [%v]", s.CMD, err) }(w) } lock.Unlock() debug("wait servises") wg.Wait() debug("all servises stoped") return exitCode } 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) for _ = range sigCh { printInfo("chld") for _, c := range services { if c.cmd.ProcessState != nil { } } } } func main() { verbose = true 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.Parse() var cfg Config err = loadConfig(&cfg, configFile) if err != nil { printErr(err) os.Exit(1) } go forwardSignals(cfg.Services) go forwardSignals2(cfg.Services) exitCode := runServices(cfg.Services) os.Exit(exitCode) }