fix: kill hook when the command is stuck (#2469)

This commit is contained in:
Ludovic Fernandez 2025-03-05 13:32:23 +01:00 committed by GitHub
commit 13780562cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 100 additions and 8 deletions

View file

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"bufio"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@ -10,6 +11,7 @@ import (
"time" "time"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/log"
) )
const ( const (
@ -32,20 +34,44 @@ func launchHook(hook string, timeout time.Duration, meta map[string]string) erro
parts := strings.Fields(hook) parts := strings.Fields(hook)
cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...) cmd.Env = append(os.Environ(), metaToEnv(meta)...)
output, err := cmdCtx.CombinedOutput() stdout, err := cmd.StdoutPipe()
if err != nil {
if len(output) > 0 { return fmt.Errorf("create pipe: %w", err)
fmt.Println(string(output))
} }
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) { cmd.Stderr = cmd.Stdout
return errors.New("hook timed out")
err = cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
} }
return err timer := time.AfterFunc(timeout, func() {
log.Println("hook timed out: killing command")
_ = cmd.Process.Kill()
_ = stdout.Close()
})
defer timer.Stop()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
err = cmd.Wait()
if err != nil {
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return fmt.Errorf("wait command: %w", err)
}
return nil
} }
func metaToEnv(meta map[string]string) []string { func metaToEnv(meta map[string]string) []string {

56
cmd/hook_test.go Normal file
View file

@ -0,0 +1,56 @@
package cmd
import (
"runtime"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_launchHook_errors(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
testCases := []struct {
desc string
hook string
timeout time.Duration
expected string
}{
{
desc: "kill the hook",
hook: "sleep 5",
timeout: 1 * time.Second,
expected: "hook timed out",
},
{
desc: "context timeout on Start",
hook: "echo foo",
timeout: 1 * time.Nanosecond,
expected: "start command: context deadline exceeded",
},
{
desc: "multiple short sleeps",
hook: "./testdata/sleepy.sh",
timeout: 1 * time.Second,
expected: "hook timed out",
},
{
desc: "long sleep",
hook: "./testdata/sleeping_beauty.sh",
timeout: 1 * time.Second,
expected: "hook timed out",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := launchHook(test.hook, test.timeout, map[string]string{})
require.EqualError(t, err, test.expected)
})
}
}

3
cmd/testdata/sleeping_beauty.sh vendored Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash -e
sleep 50

7
cmd/testdata/sleepy.sh vendored Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash -e
for i in `seq 1 10`
do
echo $i
sleep 0.2
done