Skip to content

Commit 9b1194a

Browse files
authored
Merge pull request #141 from asobrien/bugfix/shell_signals
Wait for shell when forwarding signals; shell package test coverage
2 parents 5659c08 + e4b0d64 commit 9b1194a

7 files changed

Lines changed: 184 additions & 15 deletions

File tree

main.go

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

33
import (
44
"os"
5-
"os/exec"
6-
"syscall"
5+
"github.com/gruntwork-io/terragrunt/shell"
76
"github.com/gruntwork-io/terragrunt/util"
87
"github.com/gruntwork-io/terragrunt/cli"
98
"github.com/gruntwork-io/terragrunt/errors"
@@ -35,12 +34,12 @@ func checkForErrorsAndExit(err error) {
3534
logger.Println(err)
3635
}
3736
// exit with the underlying error code
38-
var retCode int = 1
39-
if exiterr, ok := errors.Unwrap(err).(*exec.ExitError); ok {
40-
status := exiterr.Sys().(syscall.WaitStatus)
41-
retCode = status.ExitStatus()
37+
exitCode, exitCodeErr := shell.GetExitCode(err)
38+
if exitCodeErr != nil {
39+
exitCode = 1
40+
logger.Println("Unable to determine underlying exit code, so Terragrunt will exit with error code 1")
4241
}
43-
os.Exit(retCode)
42+
os.Exit(exitCode)
4443
}
4544

4645
}

shell/run_shell_cmd.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os/signal"
88
"reflect"
99
"strings"
10+
"syscall"
1011

1112
"github.com/gruntwork-io/terragrunt/errors"
1213
"github.com/gruntwork-io/terragrunt/options"
@@ -33,30 +34,45 @@ func RunShellCommand(terragruntOptions *options.TerragruntOptions, command strin
3334

3435
cmd.Dir = terragruntOptions.WorkingDir
3536

36-
signalChannel := NewSignalsForwarder(forwardSignals, cmd.Process, terragruntOptions.Logger)
37+
cmdChannel := make(chan error)
38+
signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel)
3739
defer signalChannel.Close()
3840

39-
return errors.WithStackTrace(cmd.Run())
41+
err := cmd.Run()
42+
cmdChannel <- err
43+
44+
return errors.WithStackTrace(err)
45+
}
46+
47+
48+
// Return the exit code of a command. If the error is not an exec.ExitError type,
49+
// the error is returned.
50+
func GetExitCode(err error) (int, error) {
51+
if exiterr, ok := errors.Unwrap(err).(*exec.ExitError); ok {
52+
status := exiterr.Sys().(syscall.WaitStatus)
53+
return status.ExitStatus(), nil
54+
}
55+
return 0, err
4056
}
4157

4258
type SignalsForwarder chan os.Signal
4359

44-
func NewSignalsForwarder(signals []os.Signal, p *os.Process, logger *log.Logger) SignalsForwarder {
60+
// Fowards signals to a command, waiting for the command to finish.
61+
func NewSignalsForwarder(signals []os.Signal, c *exec.Cmd, logger *log.Logger, cmdChannel chan error) SignalsForwarder {
4562
signalChannel := make(chan os.Signal, 1)
4663
signal.Notify(signalChannel, signals...)
4764

4865
go func() {
4966
for {
5067
select {
5168
case s := <-signalChannel:
52-
if s == nil {
53-
return
54-
}
5569
logger.Printf("Forward signal %s to terraform.", s.String())
56-
err := p.Signal(s)
70+
err := c.Process.Signal(s)
5771
if err != nil {
5872
logger.Printf("Error forwarding signal: %v", err)
5973
}
74+
case <- cmdChannel:
75+
return
6076
}
6177
}
6278
}()

shell/run_shell_cmd_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package shell
2+
3+
import (
4+
goerrors "errors"
5+
"github.com/gruntwork-io/terragrunt/options"
6+
"github.com/stretchr/testify/assert"
7+
"os"
8+
"os/exec"
9+
"strconv"
10+
"testing"
11+
"time"
12+
)
13+
14+
func TestRunShellCommand(t *testing.T) {
15+
t.Parallel()
16+
17+
terragruntOptions := options.NewTerragruntOptionsForTest("")
18+
cmd := RunShellCommand(terragruntOptions, "/bin/bash", "-c", "true")
19+
assert.Nil(t, cmd)
20+
21+
cmd = RunShellCommand(terragruntOptions, "/bin/bash", "-c", "false")
22+
assert.Error(t, cmd)
23+
}
24+
25+
func TestExitCode(t *testing.T) {
26+
t.Parallel()
27+
28+
for i := 0; i <= 255; i++ {
29+
cmd := exec.Command("../testdata/test_exit_code.sh", strconv.Itoa(i))
30+
err := cmd.Run()
31+
32+
if i == 0 {
33+
assert.Nil(t, err)
34+
} else {
35+
assert.Error(t, err)
36+
}
37+
retCode, err := GetExitCode(err)
38+
assert.Nil(t, err)
39+
assert.Equal(t, i, retCode)
40+
}
41+
42+
// assert a non exec.ExitError returns an error
43+
err := goerrors.New("This is an explicit error")
44+
retCode, retErr := GetExitCode(err)
45+
assert.Error(t, retErr, "An error was expected")
46+
assert.Equal(t, err, retErr)
47+
assert.Equal(t, 0, retCode)
48+
}
49+
50+
func TestNewSignalsForwarderWait(t *testing.T) {
51+
t.Parallel()
52+
53+
expectedWait := 5
54+
55+
terragruntOptions := options.NewTerragruntOptionsForTest("")
56+
cmd := exec.Command("../testdata/test_sigint_wait.sh", strconv.Itoa(expectedWait))
57+
58+
cmdChannel := make(chan error)
59+
runChannel := make(chan error)
60+
61+
signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel)
62+
defer signalChannel.Close()
63+
64+
go func() {
65+
runChannel <- cmd.Run()
66+
}()
67+
68+
time.Sleep(1000 * time.Millisecond)
69+
start := time.Now()
70+
cmd.Process.Signal(os.Interrupt)
71+
err := <-runChannel
72+
cmdChannel <- err
73+
assert.Error(t, err)
74+
retCode, err := GetExitCode(err)
75+
assert.Nil(t, err)
76+
assert.Equal(t, retCode, expectedWait)
77+
assert.WithinDuration(t, time.Now(), start.Add(time.Duration(expectedWait)*time.Second), time.Second,
78+
"Expected to wait 5 (+/-1) seconds after SIGINT")
79+
80+
}
81+
82+
func TestNewSignalsForwarderMultiple(t *testing.T) {
83+
t.Parallel()
84+
85+
expectedInterrupts := 10
86+
terragruntOptions := options.NewTerragruntOptionsForTest("")
87+
cmd := exec.Command("../testdata/test_sigint_multiple.sh", strconv.Itoa(expectedInterrupts))
88+
89+
cmdChannel := make(chan error)
90+
runChannel := make(chan error)
91+
92+
signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel)
93+
defer signalChannel.Close()
94+
95+
go func() {
96+
runChannel <- cmd.Run()
97+
}()
98+
99+
time.Sleep(1000 * time.Millisecond)
100+
101+
interruptAndWaitForProcess := func() (int, error) {
102+
var interrupts int
103+
var err error
104+
for {
105+
time.Sleep(500 * time.Millisecond)
106+
select {
107+
case err = <-runChannel:
108+
return interrupts, err
109+
default:
110+
cmd.Process.Signal(os.Interrupt)
111+
interrupts++
112+
}
113+
}
114+
}
115+
116+
interrupts, err := interruptAndWaitForProcess()
117+
cmdChannel <- err
118+
assert.Error(t, err)
119+
retCode, err := GetExitCode(err)
120+
assert.Nil(t, err)
121+
assert.Equal(t, retCode, interrupts)
122+
123+
}

shell/signal_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ import (
77
"syscall"
88
)
99

10-
var forwardSignals []os.Signal = []os.Signal{syscall.SIGTERM}
10+
var forwardSignals []os.Signal = []os.Signal{syscall.SIGTERM, syscall.SIGINT}

testdata/test_exit_code.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash -e
2+
3+
exit $1

testdata/test_sigint_multiple.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash -e
2+
3+
INT_REQUIRED=$1
4+
INT_COUNTER=0
5+
6+
trap int_handler INT
7+
8+
function int_handler() {
9+
INT_COUNTER=$((INT_COUNTER + 1))
10+
}
11+
12+
while [ $INT_COUNTER -lt $INT_REQUIRED ]
13+
do sleep 0.1
14+
done
15+
16+
exit $INT_COUNTER

testdata/test_sigint_wait.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash -e
2+
3+
WAIT_TIME=$1
4+
5+
trap int_handler INT
6+
7+
function int_handler() {
8+
sleep $WAIT_TIME
9+
exit $WAIT_TIME
10+
}
11+
12+
while true; do sleep 0.1; done

0 commit comments

Comments
 (0)