Compare commits
2 Commits
d038dafd4f
...
a69c534c11
Author | SHA1 | Date | |
---|---|---|---|
a69c534c11 | |||
a5055b226f |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
qini
|
6
Makefile
Normal file
6
Makefile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
all: build
|
||||||
|
|
||||||
|
build: qini
|
||||||
|
|
||||||
|
qini: main.go
|
||||||
|
go build -o qini main.go
|
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Qini
|
||||||
|
|
||||||
|
**Легковесный init для контейнеров**, который управляет несколькими процессами, связывая их `stdin/stdout` и гарантируя остановку всех процессов, если один из них завершается.
|
||||||
|
|
||||||
|
**Идеально для Docker-контейнеров**, чтобы избежать "висящих" процессов и обеспечить корректное завершение работы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
**Автоматическое завершение всех процессов**, если один из них остановлен
|
||||||
|
**Перенаправление stdin/stdout** между процессами
|
||||||
|
**Простая конфигурация** в формате JSON
|
||||||
|
**Минималистичный и быстрый** (написан на Go)
|
||||||
|
**Простая сборка** через `make`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### Сборка из исходников
|
||||||
|
```sh
|
||||||
|
git clone https://gitlab.stageoffice.ru/UCS-ENV/qini
|
||||||
|
cd qini
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфиг
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"cmd": "/bin/ls",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "/bin/grep",
|
||||||
|
"args": ["main"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```
|
||||||
|
$ qini -c config.json
|
||||||
|
```
|
107
main.go
107
main.go
@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -35,13 +34,9 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printErr(err error) {
|
func printErr(err error) {
|
||||||
fmt.Fprintf(os.Stderr, "[qini] Err: %s\n", 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
|
var verbose bool
|
||||||
|
|
||||||
func debug(s string, a ...any) {
|
func debug(s string, a ...any) {
|
||||||
@ -52,7 +47,7 @@ func debug(s string, a ...any) {
|
|||||||
|
|
||||||
func stopAllServices(services []*Service) {
|
func stopAllServices(services []*Service) {
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
if s.cmd.Process == nil {
|
if s.cmd.Process == nil || s.cmd.ProcessState != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,36 +62,29 @@ func stopAllServices(services []*Service) {
|
|||||||
}
|
}
|
||||||
}(s)
|
}(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
for _, s := range services {
|
|
||||||
if s.cmd.Process != nil && s.cmd.ProcessState == nil {
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServices(services []*Service) error {
|
func runServices(services []*Service) (int, error) {
|
||||||
if len(services) == 0 {
|
if len(services) == 0 {
|
||||||
return errors.New("no services in config")
|
return 0, errors.New("no services in config")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
r *io.PipeReader
|
r *os.File
|
||||||
w *io.PipeWriter
|
w *os.File
|
||||||
|
|
||||||
|
once sync.Once
|
||||||
|
exitCode int
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, s := range services {
|
for i, s := range services {
|
||||||
s.cmd = exec.Command(s.CMD, s.Args...)
|
s.cmd = exec.Command(s.CMD, s.Args...)
|
||||||
|
|
||||||
s.cmd.SysProcAttr = &syscall.SysProcAttr{
|
s.cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setsid: false,
|
//Setsid: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.PWD != "" {
|
if s.PWD != "" {
|
||||||
@ -105,10 +93,16 @@ func runServices(services []*Service) error {
|
|||||||
|
|
||||||
if r != nil {
|
if r != nil {
|
||||||
s.cmd.Stdin = r
|
s.cmd.Stdin = r
|
||||||
|
} else {
|
||||||
|
s.cmd.Stdin = os.Stdin
|
||||||
}
|
}
|
||||||
|
|
||||||
if i+1 < len(services) {
|
if i+1 < len(services) {
|
||||||
r, w = io.Pipe()
|
r, w, err = os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
s.cmd.Stdout = w
|
s.cmd.Stdout = w
|
||||||
} else {
|
} else {
|
||||||
s.cmd.Stdout = os.Stdout
|
s.cmd.Stdout = os.Stdout
|
||||||
@ -120,14 +114,26 @@ func runServices(services []*Service) error {
|
|||||||
printErr(err)
|
printErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(s *Service, w *io.PipeWriter) {
|
wg.Add(1)
|
||||||
|
go func(s *Service, w *os.File) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
s.cmd.Wait()
|
s.cmd.Wait()
|
||||||
w.Close()
|
w.Close()
|
||||||
debug("service done [%s]", s.CMD)
|
debug("service done [%s]", s.CMD)
|
||||||
|
|
||||||
|
exitCode = s.cmd.ProcessState.ExitCode()
|
||||||
|
|
||||||
|
once.Do(func() {
|
||||||
|
debug("stop all services")
|
||||||
|
stopAllServices(services)
|
||||||
|
})
|
||||||
}(s, w)
|
}(s, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
wg.Wait()
|
||||||
|
|
||||||
|
return exitCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(cfg *Config, configFile string) error {
|
func loadConfig(cfg *Config, configFile string) error {
|
||||||
@ -142,11 +148,13 @@ func loadConfig(cfg *Config, configFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forwardSignals(services []*Service) {
|
func forwardSignals(services []*Service) {
|
||||||
signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT}
|
signals := []os.Signal{syscall.SIGCHLD, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGSEGV}
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, signals...)
|
signal.Notify(sigCh, signals...)
|
||||||
|
|
||||||
for sig := range sigCh {
|
for sig := range sigCh {
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT:
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
if s.cmd.Process == nil {
|
if s.cmd.Process == nil {
|
||||||
continue
|
continue
|
||||||
@ -154,44 +162,29 @@ func forwardSignals(services []*Service) {
|
|||||||
|
|
||||||
syscall.Kill(s.cmd.Process.Pid, (sig).(syscall.Signal))
|
syscall.Kill(s.cmd.Process.Pid, (sig).(syscall.Signal))
|
||||||
}
|
}
|
||||||
}
|
case syscall.SIGCHLD:
|
||||||
}
|
|
||||||
|
|
||||||
func forwardSignals2(services []*Service) {
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGCHLD)
|
|
||||||
|
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
for range sigCh {
|
|
||||||
debug("chld")
|
debug("chld")
|
||||||
for _, c := range services {
|
|
||||||
debug("%v", c.cmd.ProcessState)
|
|
||||||
if c.cmd.ProcessState != nil {
|
|
||||||
once.Do(func() {
|
|
||||||
debug("stop all services")
|
|
||||||
stopAllServices(services)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
verbose = false
|
var (
|
||||||
|
configFile string
|
||||||
|
cfg Config
|
||||||
|
|
||||||
err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printErr(err)
|
printErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configFile string
|
|
||||||
flag.StringVar(&configFile, "c", "./config.json", "JSON config file")
|
flag.StringVar(&configFile, "c", "./config.json", "JSON config file")
|
||||||
flag.BoolVar(&verbose, "v", false, "Show debug output")
|
flag.BoolVar(&verbose, "v", false, "Show debug output")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var cfg Config
|
|
||||||
|
|
||||||
err = loadConfig(&cfg, configFile)
|
err = loadConfig(&cfg, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printErr(err)
|
printErr(err)
|
||||||
@ -199,10 +192,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go forwardSignals(cfg.Services)
|
go forwardSignals(cfg.Services)
|
||||||
go forwardSignals2(cfg.Services)
|
|
||||||
|
|
||||||
runServices(cfg.Services)
|
exitCode, err := runServices(cfg.Services)
|
||||||
//os.Exit(exitCode)
|
if err != nil {
|
||||||
|
printErr(err)
|
||||||
|
os.Exit(123)
|
||||||
|
}
|
||||||
|
|
||||||
<-ch
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user