@@ -3,13 +3,15 @@ package exec
33
44import (
55 "context"
6+ "io"
67 "os"
78 "os/exec"
89 "path/filepath"
910 "sync/atomic"
1011 "time"
1112
1213 "github.com/gruntwork-io/terragrunt/internal/os/signal"
14+ "github.com/gruntwork-io/terragrunt/internal/vexec"
1315 "github.com/gruntwork-io/terragrunt/pkg/log"
1416 "golang.org/x/text/cases"
1517 "golang.org/x/text/language"
@@ -21,78 +23,131 @@ import (
2123// gracefully after sending an interrupt signal before escalating to SIGKILL.
2224const DefaultGracefulShutdownDelay = 30 * time .Second
2325
24- // Cmd is a command type.
26+ // ErrPTYRequiresOSBackend is returned when a Cmd is started with PTY allocation
27+ // requested but the underlying vexec.Exec is not OS-backed.
28+ var ErrPTYRequiresOSBackend = errors .New ("PTY allocation requires an OS-backed vexec.Exec" )
29+
30+ // Cmd wraps a vexec.Cmd with logging, signal forwarding, and optional PTY support.
31+ // The Cmd may be backed by a real OS process or by an in-memory vexec backend
32+ // (used in tests and fuzzers to prevent fork of external binaries).
2533type Cmd struct {
26- logger log.Logger
27- interruptSignal os.Signal
28- * exec.Cmd
34+ vc vexec.Cmd
35+ osCmd * exec.Cmd
36+ logger log.Logger
37+ interruptSignal os.Signal
2938 filename string
39+ dir string
3040 forwardSignalDelay time.Duration
3141 usePTY bool
3242 gracefulShutdownRegistered atomic.Bool
3343}
3444
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 {
45+ // Command returns a `Cmd` configured to execute the named program with
46+ // the given arguments via the provided vexec.Exec. PTY allocation requires
47+ // an OS-backed Exec; non-OS backends are accepted but `WithUsePTY(true)`
48+ // will fail at Start with ErrPTYRequiresOSBackend.
49+ func Command (ctx context.Context , e vexec.Exec , name string , args ... string ) * Cmd {
50+ vc := e .Command (ctx , name , args ... )
51+
3852 cmd := & Cmd {
39- Cmd : exec . CommandContext ( ctx , name , args ... ) ,
53+ vc : vc ,
4054 logger : log .Default (),
4155 filename : filepath .Base (name ),
4256 interruptSignal : signal .InterruptSignal ,
4357 }
4458
45- cmd .Stdin = os .Stdin
46- cmd .Stdout = os .Stdout
47- cmd .Stderr = os .Stderr
59+ if osCmder , ok := vc .(vexec.OSCmder ); ok {
60+ cmd .osCmd = osCmder .OSCmd ()
61+ }
62+
63+ cmd .SetStdin (os .Stdin )
64+ cmd .SetStdout (os .Stdout )
65+ cmd .SetStderr (os .Stderr )
4866
49- cmd . WaitDelay = DefaultGracefulShutdownDelay
67+ vc . SetWaitDelay ( DefaultGracefulShutdownDelay )
5068
51- cmd . Cancel = func () error {
69+ vc . SetCancel ( func () error {
5270 if cmd .gracefulShutdownRegistered .Load () {
5371 return nil
5472 }
5573
56- if cmd .Process == nil {
57- return nil
74+ sig := signal .SignalFromContext (ctx )
75+ if sig == nil {
76+ sig = cmd .interruptSignal
5877 }
5978
60- if sig := signal . SignalFromContext ( ctx ); sig ! = nil {
61- return cmd . Process . Signal ( sig )
79+ if sig = = nil {
80+ sig = os . Kill
6281 }
6382
64- if cmd . interruptSignal != nil {
65- return cmd . Process . Signal ( cmd . interruptSignal )
83+ if err := vc . Signal ( sig ); err != nil && ! errors . Is ( err , vexec . ErrProcessNotStarted ) {
84+ return err
6685 }
6786
68- return cmd . Process . Signal ( os . Kill )
69- }
87+ return nil
88+ })
7089
7190 return cmd
7291}
7392
93+ // SetStdin sets the command's standard input.
94+ func (cmd * Cmd ) SetStdin (r io.Reader ) { cmd .vc .SetStdin (r ) }
95+
96+ // SetStdout sets the command's standard output.
97+ func (cmd * Cmd ) SetStdout (w io.Writer ) { cmd .vc .SetStdout (w ) }
98+
99+ // SetStderr sets the command's standard error.
100+ func (cmd * Cmd ) SetStderr (w io.Writer ) { cmd .vc .SetStderr (w ) }
101+
102+ // SetEnv sets the command's environment in `KEY=value` form.
103+ func (cmd * Cmd ) SetEnv (env []string ) { cmd .vc .SetEnv (env ) }
104+
105+ // SetDir sets the command's working directory.
106+ func (cmd * Cmd ) SetDir (dir string ) {
107+ cmd .dir = dir
108+ cmd .vc .SetDir (dir )
109+ }
110+
111+ // Dir returns the working directory previously set via SetDir.
112+ func (cmd * Cmd ) Dir () string { return cmd .dir }
113+
74114// Configure sets options to the `Cmd`.
75115func (cmd * Cmd ) Configure (opts ... Option ) {
76116 for _ , opt := range opts {
77117 opt (cmd )
78118 }
79119}
80120
81- // Start starts the specified command but does not wait for it to complete.
121+ // Start starts the command but does not wait for it to complete. When PTY
122+ // allocation is requested, the underlying backend must be OS-backed.
82123func (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.
85124 if cmd .usePTY {
86- if err := runCommandWithPTY ( cmd .logger , cmd . Cmd ); err ! = nil {
87- return err
125+ if cmd .osCmd = = nil {
126+ return ErrPTYRequiresOSBackend
88127 }
89- } else if err := cmd .Cmd .Start (); err != nil {
128+
129+ return runCommandWithPTY (cmd .logger , cmd .osCmd )
130+ }
131+
132+ if err := cmd .vc .Start (); err != nil {
90133 return errors .New (err )
91134 }
92135
93136 return nil
94137}
95138
139+ // Wait waits for the command to exit and returns its error.
140+ func (cmd * Cmd ) Wait () error { return cmd .vc .Wait () }
141+
142+ // Run starts the command and waits for it to complete.
143+ func (cmd * Cmd ) Run () error {
144+ if err := cmd .Start (); err != nil {
145+ return err
146+ }
147+
148+ return cmd .Wait ()
149+ }
150+
96151// RegisterGracefullyShutdown registers a graceful shutdown for the
97152// command in two ways:
98153// 1. If the context cancel contains a cause with a signal, this means
@@ -156,11 +211,13 @@ func (cmd *Cmd) ForwardSignal(ctx context.Context, sig os.Signal) {
156211 cmd .SendSignal (sig )
157212}
158213
159- // SendSignal sends the given `sig` to the executed command.
214+ // SendSignal sends the given `sig` to the executed command. Errors are logged
215+ // rather than returned; ErrProcessNotStarted is silently ignored because
216+ // callers may race against process startup.
160217func (cmd * Cmd ) SendSignal (sig os.Signal ) {
161218 cmd .logger .Debugf ("%s signal is forwarded to %s" , cases .Title (language .English ).String (sig .String ()), cmd .filename )
162219
163- if err := cmd .Process .Signal (sig ); err != nil {
220+ if err := cmd .vc .Signal (sig ); err != nil && ! errors . Is ( err , vexec . ErrProcessNotStarted ) {
164221 cmd .logger .Errorf ("Failed to forwarding signal %s to %s: %v" , sig , cmd .filename , err )
165222 }
166223}
0 commit comments