Skip to content

Commit 674b1fa

Browse files
yhakbarmowirth
andauthored
fix: Fixing signal propagation issues (#5326)
* fix: Inproper signal handling * fix: Test * fix: bot recommendations * include contribution from sylr * Switch to cmd.Cancel func override * Fix the mighty coderabbit comments * Fix sonar * Fix coderabbit * fix: Ensuring that signals properly propagate from users --------- Co-authored-by: Moritz Wirth <mw@flanga.io>
1 parent 8627f75 commit 674b1fa

9 files changed

Lines changed: 103 additions & 11 deletions

File tree

cli/commands/hcl/format/format.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/gruntwork-io/terragrunt/config"
1919
"github.com/gruntwork-io/terragrunt/internal/component"
2020
"github.com/gruntwork-io/terragrunt/internal/filter"
21+
"github.com/gruntwork-io/terragrunt/internal/os/signal"
2122
"github.com/gruntwork-io/terragrunt/pkg/log"
2223
"github.com/gruntwork-io/terragrunt/pkg/log/writer"
2324
"golang.org/x/exp/slices"
@@ -289,7 +290,33 @@ func bytesDiff(ctx context.Context, l log.Logger, b1, b2 []byte, path string) ([
289290
return nil, err
290291
}
291292

292-
data, err := exec.CommandContext(ctx, "diff", "--label="+filepath.Join("old", path), "--label="+filepath.Join("new/", path), "-u", f1.Name(), f2.Name()).CombinedOutput()
293+
diffPath, err := exec.LookPath("diff")
294+
if err != nil {
295+
return nil, fmt.Errorf("failed to find diff command in PATH: %w", err)
296+
}
297+
298+
cmd := exec.CommandContext(
299+
ctx,
300+
diffPath,
301+
"--label="+filepath.Join("old", path),
302+
"--label="+filepath.Join("new/", path),
303+
"-u",
304+
f1.Name(),
305+
f2.Name(),
306+
)
307+
cmd.Cancel = func() error {
308+
if cmd.Process == nil {
309+
return nil
310+
}
311+
312+
if sig := signal.SignalFromContext(ctx); sig != nil {
313+
return cmd.Process.Signal(sig)
314+
}
315+
316+
return cmd.Process.Signal(os.Kill)
317+
}
318+
319+
data, err := cmd.CombinedOutput()
293320
if len(data) > 0 {
294321
// diff exits with a non-zero status when the files don't match.
295322
// Ignore that failure as long as we get output.

engine/engine.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/gruntwork-io/terragrunt/internal/cache"
2121
"github.com/gruntwork-io/terragrunt/internal/experiment"
2222
"github.com/gruntwork-io/terragrunt/internal/github"
23+
"github.com/gruntwork-io/terragrunt/internal/os/signal"
2324

2425
"github.com/hashicorp/go-hclog"
2526

@@ -515,7 +516,17 @@ func createEngine(ctx context.Context, l log.Logger, terragruntOptions *options.
515516
context.WithoutCancel(ctx),
516517
localEnginePath,
517518
)
519+
cmd.Cancel = func() error {
520+
if cmd.Process == nil {
521+
return nil
522+
}
523+
524+
if sig := signal.SignalFromContext(ctx); sig != nil {
525+
return cmd.Process.Signal(sig)
526+
}
518527

528+
return cmd.Process.Signal(os.Kill)
529+
}
519530
// pass log level to engine
520531
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", engineLogLevelEnv, engineLogLevel))
521532
client := plugin.NewClient(&plugin.ClientConfig{
@@ -758,7 +769,6 @@ func ReadEngineOutput(runOptions *ExecutionOptions, forceStdErr bool, output out
758769

759770
for {
760771
response, err := output()
761-
762772
if err != nil && (errors.Is(err, ErrEngineInitFailed) || errors.Is(err, ErrEngineShutdownFailed)) {
763773
return err
764774
}

internal/git/git.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/go-git/go-git/v6"
3030
"github.com/go-git/go-git/v6/storage/filesystem"
31+
"github.com/gruntwork-io/terragrunt/internal/os/signal"
3132
"github.com/gruntwork-io/terragrunt/pkg/log"
3233
)
3334

@@ -646,6 +647,17 @@ func (g *GitRunner) SetRemoteHeadAuto(ctx context.Context) error {
646647

647648
func (g *GitRunner) prepareCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
648649
cmd := exec.CommandContext(ctx, g.GitPath, append([]string{name}, args...)...)
650+
cmd.Cancel = func() error {
651+
if cmd.Process == nil {
652+
return nil
653+
}
654+
655+
if sig := signal.SignalFromContext(ctx); sig != nil {
656+
return cmd.Process.Signal(sig)
657+
}
658+
659+
return cmd.Process.Signal(os.Kill)
660+
}
649661

650662
if g.WorkDir != "" {
651663
cmd.Dir = g.WorkDir

internal/os/exec/cmd.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ func Command(ctx context.Context, name string, args ...string) *Cmd {
3939
cmd.Stdin = os.Stdin
4040
cmd.Stdout = os.Stdout
4141
cmd.Stderr = os.Stderr
42+
cmd.Cancel = func() error {
43+
if cmd.Process == nil {
44+
return nil
45+
}
46+
47+
if sig := signal.SignalFromContext(ctx); sig != nil {
48+
return cmd.Process.Signal(sig)
49+
}
50+
51+
return cmd.Process.Signal(os.Kill)
52+
}
4253

4354
return cmd
4455
}

internal/os/signal/context_canceled.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package signal
22

33
import (
44
"context"
5+
"errors"
56
"os"
67
)
78

@@ -10,6 +11,22 @@ type ContextCanceledError struct {
1011
Signal os.Signal
1112
}
1213

14+
// SignalFromContext extracts the signal that caused the context cancellation, if any.
15+
// Returns nil if the context was not cancelled due to a signal.
16+
func SignalFromContext(ctx context.Context) os.Signal {
17+
cause := context.Cause(ctx)
18+
if cause == nil {
19+
return nil
20+
}
21+
22+
var canceledErr *ContextCanceledError
23+
if errors.As(cause, &canceledErr) && canceledErr.Signal != nil {
24+
return canceledErr.Signal
25+
}
26+
27+
return nil
28+
}
29+
1330
// NewContextCanceledError returns a new `ContextCanceledError` instance.
1431
func NewContextCanceledError(sig os.Signal) *ContextCanceledError {
1532
return &ContextCanceledError{Signal: sig}

shell/run_cmd_unix_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ func TestRunCommandWithOutputInterrupt(t *testing.T) {
5151
expectedExitStatusErr := fmt.Sprintf("Failed to execute \"%s 5\" in .\n\nexit status %d", cmdPath, expectedWait)
5252
expectedKilledErr := fmt.Sprintf("Failed to execute \"%s 5\" in .\n\nsignal: killed", cmdPath)
5353

54-
if actualErr.Error() != expectedExitStatusErr && actualErr.Error() != expectedKilledErr {
55-
t.Errorf("Expected error to be either:\n %s\nor:\n %s\nbut got:\n %s",
56-
expectedExitStatusErr, expectedKilledErr, actualErr.Error())
54+
if actualErr.Error() == expectedKilledErr {
55+
t.Errorf("Expected process to gracefully terminate but got\n: %s", actualErr.Error())
56+
} else if actualErr.Error() != expectedExitStatusErr {
57+
t.Errorf("Expected error to be:\n %s\nbut got:\n %s",
58+
expectedExitStatusErr, actualErr.Error())
5759
}
5860
}

shell/run_cmd_windows_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,13 @@ func TestWindowsRunCommandWithOutputInterrupt(t *testing.T) {
5858
containsKilled := strings.Contains(actualErrStr, "signal: killed")
5959
containsFailedExecute := strings.Contains(actualErrStr, fmt.Sprintf("Failed to execute \"%s", cmdPath))
6060

61+
if containsKilled {
62+
t.Errorf("Expected process to gracefully terminate but got\n: %s", actualErrStr)
63+
}
64+
6165
// On Windows, the batch file might exit with status 1 when interrupted, or be killed by signal
62-
if !containsFailedExecute || (!containsExitStatus5 && !containsExitStatus1 && !containsKilled) {
63-
t.Errorf("Expected error to contain 'Failed to execute \"%s' and either 'exit status 5', 'exit status 1', or 'signal: killed', but got:\n %s",
66+
if !containsFailedExecute || (!containsExitStatus5 && !containsExitStatus1) {
67+
t.Errorf("Expected error to contain 'Failed to execute \"%s' and either 'exit status 5', or 'exit status 1', but got:\n %s",
6468
cmdPath, actualErrStr)
6569
}
6670
}

test/helpers/test_helpers.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313

1414
"github.com/gruntwork-io/terragrunt/internal/component"
15+
"github.com/gruntwork-io/terragrunt/internal/os/signal"
1516
"github.com/gruntwork-io/terragrunt/options"
1617
"github.com/stretchr/testify/require"
1718
)
@@ -148,6 +149,17 @@ func ExecWithTestLogger(t *testing.T, dir, command string, args ...string) {
148149
ctx := t.Context()
149150
cmd := exec.CommandContext(ctx, command, args...)
150151
cmd.Dir = dir
152+
cmd.Cancel = func() error {
153+
if cmd.Process == nil {
154+
return nil
155+
}
156+
157+
if sig := signal.SignalFromContext(ctx); sig != nil {
158+
return cmd.Process.Signal(sig)
159+
}
160+
161+
return cmd.Process.Signal(os.Kill)
162+
}
151163

152164
var stdout, stderr bytes.Buffer
153165

util/shell.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"os/exec"
78
"strings"
89
"syscall"
910

10-
"os/exec"
11-
1211
"github.com/gruntwork-io/terragrunt/internal/cli"
1312
"github.com/gruntwork-io/terragrunt/internal/errors"
1413
)
@@ -44,13 +43,11 @@ func GetExitCode(err error) (int, error) {
4443
var exitStatus interface {
4544
ExitStatus() (int, error)
4645
}
47-
4846
if errors.As(err, &exitStatus) {
4947
return exitStatus.ExitStatus()
5048
}
5149

5250
var exitCoder cli.ExitCoder
53-
5451
if errors.As(err, &exitCoder) {
5552
return exitCoder.ExitCode(), nil
5653
}

0 commit comments

Comments
 (0)