mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-15 20:02:15 +01:00
refactoring process management: no more orphan processes and quicker chrome startup
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user