9
9
"os/exec"
10
10
"sync/atomic"
11
11
"syscall"
12
- "time"
13
12
14
13
"golang.org/x/sync/errgroup"
15
14
)
@@ -98,11 +97,8 @@ func (s *commandStage) Start(
98
97
})
99
98
}
100
99
101
- // Put the command in its own process group:
102
- if s .cmd .SysProcAttr == nil {
103
- s .cmd .SysProcAttr = & syscall.SysProcAttr {}
104
- }
105
- s .cmd .SysProcAttr .Setpgid = true
100
+ // Put the command in its own process group, if possible:
101
+ s .runInOwnProcessGroup ()
106
102
107
103
if err := s .cmd .Start (); err != nil {
108
104
return nil , err
@@ -122,53 +118,6 @@ func (s *commandStage) Start(
122
118
return stdout , nil
123
119
}
124
120
125
- // kill is called to kill the process if the context expires. `err` is
126
- // the corresponding value of `Context.Err()`.
127
- func (s * commandStage ) kill (err error ) {
128
- // I believe that the calls to `syscall.Kill()` in this method are
129
- // racy. It could be that s.cmd.Wait() succeeds immediately before
130
- // this call, in which case the process group wouldn't exist
131
- // anymore. But I don't see any way to avoid this without
132
- // duplicating a lot of code from `exec.Cmd`. (`os.Cmd.Kill()` and
133
- // `os.Cmd.Signal()` appear to be race-free, but only because they
134
- // use internal synchronization. But those methods only kill the
135
- // process, not the process group, so they are not suitable here.
136
-
137
- // We started the process with PGID == PID:
138
- pid := s .cmd .Process .Pid
139
- select {
140
- case <- s .done :
141
- // Process has ended; no need to kill it again.
142
- return
143
- default :
144
- }
145
-
146
- // Record the `ctx.Err()`, which will be used as the error result
147
- // for this stage.
148
- s .ctxErr .Store (err )
149
-
150
- // First try to kill using a relatively gentle signal so that
151
- // the processes have a chance to clean up after themselves:
152
- _ = syscall .Kill (- pid , syscall .SIGTERM )
153
-
154
- // Well-behaved processes should commit suicide after the above,
155
- // but if they don't exit within 2s, murder the whole lot of them:
156
- go func () {
157
- // Use an explicit `time.Timer` rather than `time.After()` so
158
- // that we can stop it (freeing resources) promptly if the
159
- // command exits before the timer triggers.
160
- timer := time .NewTimer (2 * time .Second )
161
- defer timer .Stop ()
162
-
163
- select {
164
- case <- s .done :
165
- // Process has ended; no need to kill it again.
166
- case <- timer .C :
167
- _ = syscall .Kill (- pid , syscall .SIGKILL )
168
- }
169
- }()
170
- }
171
-
172
121
// filterCmdError interprets `err`, which was returned by `Cmd.Wait()`
173
122
// (possibly `nil`), possibly modifying it or ignoring it. It returns
174
123
// the error that should actually be returned to the caller (possibly
@@ -186,7 +135,10 @@ func (s *commandStage) filterCmdError(err error) error {
186
135
ctxErr , ok := s .ctxErr .Load ().(error )
187
136
if ok {
188
137
// If the process looks like it was killed by us, substitute
189
- // `ctxErr` for the process's own exit error.
138
+ // `ctxErr` for the process's own exit error. Note that this
139
+ // doesn't do anything on Windows, where the `Signaled()`
140
+ // method isn't implemented (it is hardcoded to return
141
+ // `false`).
190
142
ps , ok := eErr .ProcessState .Sys ().(syscall.WaitStatus )
191
143
if ok && ps .Signaled () &&
192
144
(ps .Signal () == syscall .SIGTERM || ps .Signal () == syscall .SIGKILL ) {
0 commit comments