202 lines
3.3 KiB
Go
202 lines
3.3 KiB
Go
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 {
|
|
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}
|
|
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() {
|
|
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)
|
|
|
|
exitCode, err := runServices(cfg.Services)
|
|
if err != nil {
|
|
printErr(err)
|
|
os.Exit(123)
|
|
}
|
|
|
|
os.Exit(exitCode)
|
|
}
|