proper handle nested subprocesses on Linux

This commit is contained in:
Alexander Baryshnikov 2020-10-10 17:41:20 +08:00
parent b6fd8e1eb2
commit 38935d4657
5 changed files with 94 additions and 42 deletions

View File

@ -3,6 +3,11 @@
Binary modes just executes any script in shell (`/bin/sh` by default). You can override shell per-unit
by `shell: /path/to/shell` configuration param.
Linux's hosts should not worry about "leaking" processes (when process
creates number of background subprocesses) - it wil be cleaned automatically.
**For non-Linux users**
To handle a graceful timeout, child should be able to forward signal: basically, use `exec` before last command.
Danger (but will work), signals may not be handled by foo

View File

@ -1,9 +0,0 @@
// +build !linux
package api
import "os/exec"
func SetBinFlags(cmd *exec.Cmd) {
}

20
server/internal/flags.go Normal file
View File

@ -0,0 +1,20 @@
// +build !linux
package internal
import (
"os"
"os/exec"
)
func SetBinFlags(cmd *exec.Cmd) {
}
func IntSignal(cmd *exec.Cmd) error {
return cmd.Process.Signal(os.Interrupt)
}
func KillSignal(cmd *exec.Cmd) error {
return cmd.Process.Signal(os.Kill)
}

View File

@ -1,4 +1,4 @@
package api
package internal
import (
"os/exec"
@ -12,3 +12,11 @@ func SetBinFlags(cmd *exec.Cmd) {
cmd.SysProcAttr.Pdeathsig = syscall.SIGTERM
cmd.SysProcAttr.Setpgid = true
}
func IntSignal(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
}
func KillSignal(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}

View File

@ -11,7 +11,7 @@ import (
"strings"
"time"
"nano-run/server/api"
"nano-run/server/internal"
)
type markerResponse struct {
@ -44,15 +44,8 @@ type binHandler struct {
func (bh *binHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
marker := &markerResponse{res: writer}
ctx := request.Context()
if bh.timeout > 0 {
c, cancel := context.WithTimeout(ctx, bh.timeout)
defer cancel()
ctx = c
}
cmd := exec.CommandContext(ctx, bh.shell, "-c", bh.command) //nolint:gosec
cmd := exec.Command(bh.shell, "-c", bh.command) //nolint:gosec
if bh.workDir == "" {
tmpDir, err := ioutil.TempDir("", "")
@ -76,30 +69,9 @@ func (bh *binHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
cmd.Stdin = request.Body
cmd.Stdout = marker
cmd.Env = env
api.SetBinFlags(cmd)
internal.SetBinFlags(cmd)
var done bool
if bh.gracefulTimeout > 0 {
graceCtx, graceCancel := context.WithTimeout(ctx, bh.gracefulTimeout)
defer graceCancel()
go func() {
<-graceCtx.Done()
proc := cmd.Process
if proc == nil || done {
return
}
err := proc.Signal(os.Interrupt)
if err != nil {
log.Println("failed send signal to process:", err)
} else {
log.Println("sent graceful shutdown to proces")
}
}()
}
err := cmd.Run()
done = true
err := bh.run(ctx, cmd)
if codeReset, ok := writer.(interface{ Status(status int) }); ok && err != nil {
codeReset.Status(http.StatusBadGateway)
@ -114,6 +86,62 @@ func (bh *binHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
}
}
func (bh *binHandler) run(global context.Context, cmd *exec.Cmd) error {
err := cmd.Start()
if err != nil {
return err
}
var (
gracefulTimeout <-chan time.Time
ctx context.Context
)
var gracefulTimer *time.Ticker
if bh.gracefulTimeout > 0 {
gracefulTimer = time.NewTicker(bh.gracefulTimeout)
defer gracefulTimer.Stop()
gracefulTimeout = gracefulTimer.C
}
if bh.timeout > 0 {
t, cancel := context.WithTimeout(global, bh.timeout)
defer cancel()
ctx = t
} else {
ctx = global
}
var process = make(chan error, 1)
go func() {
defer close(process)
process <- cmd.Wait()
}()
for {
select {
case <-gracefulTimeout:
if err := internal.IntSignal(cmd); err != nil {
log.Println("failed send signal to process:", err)
} else {
log.Println("sent graceful shutdown to process")
}
gracefulTimer.Stop()
gracefulTimeout = nil
case <-ctx.Done():
if err := internal.KillSignal(cmd); err != nil {
log.Println("failed send kill to process:", err)
} else {
log.Println("sent kill to process")
}
return <-process
case err := <-process:
return err
}
}
}
func (bh *binHandler) cloneEnv() []string {
var cp = make([]string, len(bh.environment))
copy(cp, bh.environment)