first commit

This commit is contained in:
2025-07-23 12:23:38 +03:00
commit 1fe3db41c2
4 changed files with 233 additions and 0 deletions

16
config.json Normal file
View File

@ -0,0 +1,16 @@
{
"services": [
{
"cmd": "/bin/sleep",
"args": ["100"]
},
{
"cmd": "/bin/cat",
"args": []
},
{
"cmd": "/bin/grep",
"args": ["main"]
}
]
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module qini
go 1.24.5
require golang.org/x/sys v0.34.0

2
go.sum Normal file
View File

@ -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=

210
main.go Normal file
View File

@ -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)
}