refactoring process management: no more orphan processes and quicker chrome startup

This commit is contained in:
Julien Neuhart
2019-10-02 10:38:47 +02:00
parent c47dc5ecdb
commit 7ac104c3c6
8 changed files with 187 additions and 137 deletions

View File

@@ -1,6 +1,7 @@
/*
Package xexec helps creating exec.Cmd
with logging.
with logging and executing those commands
without leaking orphan processes.
All functions return our standard xerror.Error
in case of error.

View File

@@ -8,6 +8,7 @@ import (
"io"
"os/exec"
"strings"
"syscall"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
@@ -43,6 +44,61 @@ func CommandContext(ctx context.Context, logger xlog.Logger, binary string, args
return cmd, nil
}
/*
Run runs a command.
If command finishes or fails to finish
before context.Context deadline, kill the
corresponding process in a way which does
not leak orphan processes.
*/
func Run(ctx context.Context, logger xlog.Logger, binary string, args ...string) error {
const op string = "xexec.Run"
resolver := func() error {
cmd, err := Command(
logger,
binary,
args...,
)
if err != nil {
return err
}
LogBeforeExecute(logger, cmd)
// see https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
kill := func() {
err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
if err == nil {
return
}
if !strings.Contains(err.Error(), "no such process") {
logger.ErrorOp(op, err)
}
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
return err
}
result := make(chan error, 1)
go func() {
result <- cmd.Wait()
}()
select {
case err := <-result:
logger.DebugfOp(op, "command '%s' finished", strings.Join(cmd.Args, " "))
kill()
return err
case <-ctx.Done():
logger.DebugfOp(op, "command '%s' failed to finish before context.Context deadline", strings.Join(cmd.Args, " "))
kill()
return ctx.Err()
}
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// LogBeforeExecute logs a command before its execution.
func LogBeforeExecute(logger xlog.Logger, cmd *exec.Cmd) {
const op string = "xexec.LogBeforeExecute"
@@ -87,7 +143,7 @@ func logCommandOutput(logger xlog.Logger, reader io.ReadCloser, outputType strin
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF {
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
logger.ErrorOp(op, err)
}
break

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
"github.com/thecodingmachine/gotenberg/test"
)
@@ -37,3 +38,16 @@ func TestCommandContext(t *testing.T) {
LogBeforeExecute(logger, cmd)
assert.Nil(t, err)
}
func TestRun(t *testing.T) {
logger := test.DebugLogger()
// should run without issue.
err := Run(context.Background(), logger, "echo", "Hello", "World")
assert.Nil(t, err)
// should not be OK as context.Context
// should timeout.
ctx, cancel := xcontext.WithTimeout(logger, 0)
defer cancel()
err = Run(ctx, logger, "echo", "Hello", "World")
assert.NotNil(t, err)
}