commit 1fe3db41c29cf94c8b0b607dc985a1ada7926946 Author: dedal.qq Date: Wed Jul 23 12:23:38 2025 +0300 first commit diff --git a/config.json b/config.json new file mode 100644 index 0000000..b244565 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "services": [ + { + "cmd": "/bin/sleep", + "args": ["100"] + }, + { + "cmd": "/bin/cat", + "args": [] + }, + { + "cmd": "/bin/grep", + "args": ["main"] + } + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20f3d2b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module qini + +go 1.24.5 + +require golang.org/x/sys v0.34.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a197cce --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/main.go b/main.go new file mode 100644 index 0000000..45f2fb4 --- /dev/null +++ b/main.go @@ -0,0 +1,210 @@ +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) +}