@@ -3,13 +3,14 @@ package exec
33
44import (
55 "context"
6+ "io"
67 "os"
7- "os/exec"
88 "path/filepath"
99 "sync/atomic"
1010 "time"
1111
1212 "github.com/gruntwork-io/terragrunt/internal/os/signal"
13+ "github.com/gruntwork-io/terragrunt/internal/vexec"
1314 "github.com/gruntwork-io/terragrunt/pkg/log"
1415 "golang.org/x/text/cases"
1516 "golang.org/x/text/language"
@@ -21,78 +22,125 @@ import (
2122// gracefully after sending an interrupt signal before escalating to SIGKILL.
2223const DefaultGracefulShutdownDelay = 30 * time .Second
2324
24- // Cmd is a command type.
25+ // ErrPTYRequiresOSBackend is returned when a Cmd is started with PTY allocation
26+ // requested but the underlying vexec.Exec is not OS-backed.
27+ var ErrPTYRequiresOSBackend = errors .New ("PTY allocation requires an OS-backed vexec.Exec" )
28+
29+ // Cmd wraps a vexec.Cmd with signal forwarding and optional PTY support.
30+ // The Cmd may be backed by a real OS process or by an in-memory vexec backend
31+ // (used in tests and fuzzers to prevent fork of external binaries).
2532type Cmd struct {
26- logger log.Logger
27- interruptSignal os.Signal
28- * exec.Cmd
33+ vc vexec.Cmd
34+ interruptSignal os.Signal
2935 filename string
36+ dir string
3037 forwardSignalDelay time.Duration
3138 usePTY bool
3239 gracefulShutdownRegistered atomic.Bool
3340}
3441
35- // Command returns the `Cmd` struct to execute the named program with
36- // the given arguments.
37- func Command (ctx context.Context , name string , args ... string ) * Cmd {
42+ // Command returns a `Cmd` configured to execute the named program with
43+ // the given arguments via the provided vexec.Exec. PTY allocation requires
44+ // an OS-backed Exec; non-OS backends are accepted but `WithUsePTY(true)`
45+ // will fail at Start with ErrPTYRequiresOSBackend.
46+ func Command (ctx context.Context , e vexec.Exec , name string , args ... string ) * Cmd {
47+ vc := e .Command (ctx , name , args ... )
48+
3849 cmd := & Cmd {
39- Cmd : exec .CommandContext (ctx , name , args ... ),
40- logger : log .Default (),
50+ vc : vc ,
4151 filename : filepath .Base (name ),
4252 interruptSignal : signal .InterruptSignal ,
4353 }
4454
45- cmd .Stdin = os .Stdin
46- cmd .Stdout = os .Stdout
47- cmd .Stderr = os .Stderr
55+ cmd .SetStdin ( os .Stdin )
56+ cmd .SetStdout ( os .Stdout )
57+ cmd .SetStderr ( os .Stderr )
4858
49- cmd . WaitDelay = DefaultGracefulShutdownDelay
59+ vc . SetWaitDelay ( DefaultGracefulShutdownDelay )
5060
51- cmd . Cancel = func () error {
61+ vc . SetCancel ( func () error {
5262 if cmd .gracefulShutdownRegistered .Load () {
5363 return nil
5464 }
5565
56- if cmd .Process == nil {
57- return nil
66+ sig := signal .SignalFromContext (ctx )
67+ if sig == nil {
68+ sig = cmd .interruptSignal
5869 }
5970
60- if sig := signal . SignalFromContext ( ctx ); sig ! = nil {
61- return cmd . Process . Signal ( sig )
71+ if sig = = nil {
72+ sig = os . Kill
6273 }
6374
64- if cmd . interruptSignal != nil {
65- return cmd . Process . Signal ( cmd . interruptSignal )
75+ if err := vc . Signal ( sig ); err != nil && ! errors . Is ( err , vexec . ErrProcessNotStarted ) {
76+ return err
6677 }
6778
68- return cmd . Process . Signal ( os . Kill )
69- }
79+ return nil
80+ })
7081
7182 return cmd
7283}
7384
85+ // SetStdin sets the command's standard input.
86+ func (cmd * Cmd ) SetStdin (r io.Reader ) { cmd .vc .SetStdin (r ) }
87+
88+ // SetStdout sets the command's standard output.
89+ func (cmd * Cmd ) SetStdout (w io.Writer ) { cmd .vc .SetStdout (w ) }
90+
91+ // SetStderr sets the command's standard error.
92+ func (cmd * Cmd ) SetStderr (w io.Writer ) { cmd .vc .SetStderr (w ) }
93+
94+ // SetEnv sets the command's environment in `KEY=value` form.
95+ func (cmd * Cmd ) SetEnv (env []string ) { cmd .vc .SetEnv (env ) }
96+
97+ // SetDir sets the command's working directory.
98+ func (cmd * Cmd ) SetDir (dir string ) {
99+ cmd .dir = dir
100+ cmd .vc .SetDir (dir )
101+ }
102+
103+ // Dir returns the working directory previously set via SetDir.
104+ func (cmd * Cmd ) Dir () string { return cmd .dir }
105+
74106// Configure sets options to the `Cmd`.
75107func (cmd * Cmd ) Configure (opts ... Option ) {
76108 for _ , opt := range opts {
77109 opt (cmd )
78110 }
79111}
80112
81- // Start starts the specified command but does not wait for it to complete.
82- func (cmd * Cmd ) Start () error {
83- // If we need to allocate a ptty for the command, route through the ptty routine.
84- // Otherwise, directly call the command.
113+ // Start starts the command but does not wait for it to complete. When PTY
114+ // allocation is requested, the underlying backend must be OS-backed.
115+ func (cmd * Cmd ) Start (l log.Logger ) error {
85116 if cmd .usePTY {
86- if err := runCommandWithPTY (cmd .logger , cmd .Cmd ); err != nil {
87- return err
117+ osCmder , ok := cmd .vc .(vexec.OSCmder )
118+ if ! ok {
119+ return ErrPTYRequiresOSBackend
88120 }
89- } else if err := cmd .Cmd .Start (); err != nil {
121+
122+ return runCommandWithPTY (l , osCmder .OSCmd ())
123+ }
124+
125+ if err := cmd .vc .Start (); err != nil {
90126 return errors .New (err )
91127 }
92128
93129 return nil
94130}
95131
132+ // Wait waits for the command to exit and returns its error.
133+ func (cmd * Cmd ) Wait () error { return cmd .vc .Wait () }
134+
135+ // Run starts the command and waits for it to complete.
136+ func (cmd * Cmd ) Run (l log.Logger ) error {
137+ if err := cmd .Start (l ); err != nil {
138+ return err
139+ }
140+
141+ return cmd .Wait ()
142+ }
143+
96144// RegisterGracefullyShutdown registers a graceful shutdown for the
97145// command in two ways:
98146// 1. If the context cancel contains a cause with a signal, this means
@@ -106,7 +154,7 @@ func (cmd *Cmd) Start() error {
106154// was some failure and we need to terminate all executed commands,
107155// in this situation we are sure that commands did not receive any
108156// signal, so we send them an interrupt signal immediately.
109- func (cmd * Cmd ) RegisterGracefullyShutdown (ctx context.Context ) func () {
157+ func (cmd * Cmd ) RegisterGracefullyShutdown (ctx context.Context , l log. Logger ) func () {
110158 cmd .gracefulShutdownRegistered .Store (true )
111159
112160 ctxShutdown , cancelShutdown := context .WithCancel (context .Background ())
@@ -116,12 +164,12 @@ func (cmd *Cmd) RegisterGracefullyShutdown(ctx context.Context) func() {
116164 case <- ctxShutdown .Done ():
117165 case <- ctx .Done ():
118166 if cause := new (signal.ContextCanceledError ); errors .As (context .Cause (ctx ), & cause ) && cause .Signal != nil {
119- cmd .ForwardSignal (ctxShutdown , cause .Signal )
167+ cmd .ForwardSignal (ctxShutdown , l , cause .Signal )
120168
121169 return
122170 }
123171
124- cmd .SendSignal (cmd .interruptSignal )
172+ cmd .SendSignal (l , cmd .interruptSignal )
125173 }
126174 }()
127175
@@ -130,7 +178,7 @@ func (cmd *Cmd) RegisterGracefullyShutdown(ctx context.Context) func() {
130178
131179// ForwardSignal forwards a given `sig` with a delay if cmd.forwardSignalDelay is greater than 0,
132180// and if the same signal is received again, it is forwarded immediately.
133- func (cmd * Cmd ) ForwardSignal (ctx context.Context , sig os.Signal ) {
181+ func (cmd * Cmd ) ForwardSignal (ctx context.Context , l log. Logger , sig os.Signal ) {
134182 ctxDelay , cancelDelay := context .WithCancel (ctx )
135183 defer cancelDelay ()
136184
@@ -139,7 +187,7 @@ func (cmd *Cmd) ForwardSignal(ctx context.Context, sig os.Signal) {
139187 }, sig )
140188
141189 if cmd .forwardSignalDelay > 0 {
142- cmd . logger .Debugf ("%s signal will be forwarded to %s with delay %s" ,
190+ l .Debugf ("%s signal will be forwarded to %s with delay %s" ,
143191 cases .Title (language .English ).String (sig .String ()),
144192 cmd .filename ,
145193 cmd .forwardSignalDelay ,
@@ -153,14 +201,16 @@ func (cmd *Cmd) ForwardSignal(ctx context.Context, sig os.Signal) {
153201 case <- ctxDelay .Done ():
154202 }
155203
156- cmd .SendSignal (sig )
204+ cmd .SendSignal (l , sig )
157205}
158206
159- // SendSignal sends the given `sig` to the executed command.
160- func (cmd * Cmd ) SendSignal (sig os.Signal ) {
161- cmd .logger .Debugf ("%s signal is forwarded to %s" , cases .Title (language .English ).String (sig .String ()), cmd .filename )
207+ // SendSignal sends the given `sig` to the executed command. Errors are logged
208+ // rather than returned; ErrProcessNotStarted is silently ignored because
209+ // callers may race against process startup.
210+ func (cmd * Cmd ) SendSignal (l log.Logger , sig os.Signal ) {
211+ l .Debugf ("%s signal is forwarded to %s" , cases .Title (language .English ).String (sig .String ()), cmd .filename )
162212
163- if err := cmd .Process .Signal (sig ); err != nil {
164- cmd . logger .Errorf ("Failed to forwarding signal %s to %s: %v" , sig , cmd .filename , err )
213+ if err := cmd .vc .Signal (sig ); err != nil && ! errors . Is ( err , vexec . ErrProcessNotStarted ) {
214+ l .Errorf ("Failed to forwarding signal %s to %s: %v" , sig , cmd .filename , err )
165215 }
166216}
0 commit comments