1
1
package buildbox
2
2
3
+ // Logic for this file is largely based on:
4
+ // https://github.com/jarib/childprocess/blob/783f7a00a1678b5d929062564ef5ae76822dfd62/lib/childprocess/unix/process.rb
5
+
3
6
import (
4
7
"bytes"
8
+ "errors"
5
9
"fmt"
6
10
"github.com/kr/pty"
7
11
"io"
8
12
"os"
9
13
"os/exec"
10
14
"path"
11
15
"path/filepath"
12
- "regexp"
13
16
"sync"
17
+ "syscall"
14
18
"time"
15
19
)
16
20
@@ -20,55 +24,63 @@ type Process struct {
20
24
Running bool
21
25
ExitStatus string
22
26
command * exec.Cmd
27
+ callback func (* Process )
23
28
}
24
29
25
30
// Implement the Stringer thingy
26
31
func (p Process ) String () string {
27
32
return fmt .Sprintf ("Process{Pid: %d, Running: %t, ExitStatus: %s}" , p .Pid , p .Running , p .ExitStatus )
28
33
}
29
34
30
- func (p Process ) Kill () error {
31
- return p .command .Process .Kill ()
32
- }
33
-
34
- func RunScript (dir string , script string , env []string , callback func (Process )) (* Process , error ) {
35
+ func InitProcess (dir string , script string , env []string , callback func (* Process )) * Process {
35
36
// Create a new instance of our process struct
36
37
var process Process
37
38
38
39
// Find the script to run
39
40
absoluteDir , _ := filepath .Abs (dir )
40
41
pathToScript := path .Join (absoluteDir , script )
41
42
42
- Logger .Infof ("Starting to run script `%s` from inside %s" , script , absoluteDir )
43
-
44
43
process .command = exec .Command (pathToScript )
45
44
process .command .Dir = absoluteDir
46
45
46
+ // Children of the forked process will inherit its process group
47
+ // This is to make sure that all grandchildren dies when this Process instance is killed
48
+ process .command .SysProcAttr = & syscall.SysProcAttr {Setpgid : true }
49
+
47
50
// Copy the current processes ENV and merge in the
48
51
// new ones. We do this so the sub process gets PATH
49
52
// and stuff.
50
53
// TODO: Is this correct?
51
54
currentEnv := os .Environ ()
52
55
process .command .Env = append (currentEnv , env ... )
53
56
57
+ // Set the callback
58
+ process .callback = callback
59
+
60
+ return & process
61
+ }
62
+
63
+ func (p * Process ) Start () error {
64
+ Logger .Infof ("Starting to run script: %s" , p .command .Path )
65
+
54
66
// Start our process
55
- pty , err := pty .Start (process .command )
67
+ pty , err := pty .Start (p .command )
56
68
if err != nil {
57
69
// The process essentially failed, so we'll just make up
58
70
// and exit status.
59
- process .ExitStatus = "1"
71
+ p .ExitStatus = "1"
60
72
61
- return & process , err
73
+ return err
62
74
}
63
75
64
- process .Pid = process .command .Process .Pid
65
- process .Running = true
76
+ p .Pid = p .command .Process .Pid
77
+ p .Running = true
66
78
67
- Logger .Infof ("Process is running with PID: %d" , process .Pid )
79
+ Logger .Infof ("Process is running with PID: %d" , p .Pid )
68
80
69
81
var buffer bytes.Buffer
70
- var w sync.WaitGroup
71
- w .Add (2 )
82
+ var waitGroup sync.WaitGroup
83
+ waitGroup .Add (2 )
72
84
73
85
go func () {
74
86
Logger .Debug ("Starting to copy PTY to the buffer" )
@@ -82,86 +94,167 @@ func RunScript(dir string, script string, env []string, callback func(Process))
82
94
Logger .Debug ("io.Copy finsihed" )
83
95
}
84
96
85
- w .Done ()
97
+ waitGroup .Done ()
86
98
}()
87
99
88
100
go func () {
89
- for process .Running {
101
+ for p .Running {
90
102
Logger .Debug ("Copying buffer to the process output" )
91
103
92
104
// Convert the stdout buffer to a string
93
- process .Output = buffer .String ()
105
+ p .Output = buffer .String ()
94
106
95
107
// Call the callback and pass in our process object
96
- callback (process )
108
+ p . callback (p )
97
109
98
110
// Sleep for 1 second
99
111
time .Sleep (1000 * time .Millisecond )
100
112
}
101
113
102
114
Logger .Debug ("Finished routine that copies the buffer to the process output" )
103
115
104
- w .Done ()
116
+ waitGroup .Done ()
105
117
}()
106
118
107
119
// Wait until the process has finished. The returned error is nil if the command runs,
108
120
// has no problems copying stdin, stdout, and stderr, and exits with a zero exit status.
109
- waitResult := process .command .Wait ()
121
+ waitResult := p .command .Wait ()
110
122
111
123
// The process is no longer running at this point
112
- process .Running = false
124
+ p .Running = false
113
125
114
- // Determine the exit status (if waitResult is an error, that means that the process
115
- // returned a non zero exit status)
116
- if waitResult != nil {
117
- if werr , ok := waitResult .(* exec.ExitError ); ok {
118
- // This returns a string like: `exit status 123`
119
- exitString := werr .Error ()
120
- exitStringRegex := regexp .MustCompile (`([0-9]+)$` )
126
+ // Find the exit status of the script
127
+ p .ExitStatus = getExitStatus (waitResult )
121
128
122
- if exitStringRegex .MatchString (exitString ) {
123
- process .ExitStatus = exitStringRegex .FindString (exitString )
124
- } else {
125
- Logger .Errorf ("Weird looking exit status: %s" , exitString )
129
+ Logger .Debugf ("Process with PID: %d finished with Exit Status: %s" , p .Pid , p .ExitStatus )
126
130
127
- // If the exit status isn't what I'm looking for, provide a generic one.
128
- process .ExitStatus = "-1"
131
+ // Sometimes (in docker containers) io.Copy never seems to finish. This is a mega
132
+ // hack around it. If it doesn't finish after 1 second, just continue.
133
+ Logger .Debug ("Waiting for io.Copy and incremental output to finish" )
134
+ err = timeoutWait (& waitGroup )
135
+ if err != nil {
136
+ Logger .Errorf ("Timed out waiting for wait group: (%T: %v)" , err , err )
137
+ }
138
+
139
+ // Copy the final output back to the process
140
+ p .Output = buffer .String ()
141
+
142
+ // No error occured so we can return nil
143
+ return nil
144
+ }
145
+
146
+ func (p * Process ) Kill () error {
147
+ // Send a sigterm
148
+ err := p .signal (syscall .SIGTERM )
149
+ if err != nil {
150
+ return err
151
+ }
152
+
153
+ // Make a chanel that we'll use as a timeout
154
+ c := make (chan int , 1 )
155
+ checking := true
156
+
157
+ // Start a routine that checks to see if the process
158
+ // is still alive.
159
+ go func () {
160
+ for checking {
161
+ Logger .Debugf ("Checking to see if PID: %d is still alive" , p .Pid )
162
+
163
+ foundProcess , err := os .FindProcess (p .Pid )
164
+
165
+ // Can't find the process at all
166
+ if err != nil {
167
+ Logger .Debugf ("Could not find process with PID: %d" , p .Pid )
168
+
169
+ break
129
170
}
130
- } else {
131
- Logger .Errorf ("Could not determine exit status. %T: %v" , waitResult , waitResult )
132
171
133
- // Not sure what to provide as an exit status if one couldn't be determined.
134
- process .ExitStatus = "-1"
172
+ // We have some information about the procss
173
+ if foundProcess != nil {
174
+ processState , err := foundProcess .Wait ()
175
+
176
+ if err != nil || processState .Exited () {
177
+ Logger .Debugf ("Process with PID: %d has exited." , p .Pid )
178
+
179
+ break
180
+ }
181
+ }
182
+
183
+ // Retry in a moment
184
+ sleepTime := time .Duration (1 * time .Second )
185
+ time .Sleep (sleepTime )
186
+ }
187
+
188
+ c <- 1
189
+ }()
190
+
191
+ // Timeout this process after 3 seconds
192
+ select {
193
+ case _ = <- c :
194
+ // Was successfully terminated
195
+ case <- time .After (5 * time .Second ):
196
+ // Stop checking in the routine above
197
+ checking = false
198
+
199
+ // Forcefully kill the thing
200
+ err = p .signal (syscall .SIGKILL )
201
+
202
+ if err != nil {
203
+ return err
204
+ }
205
+ }
206
+
207
+ return nil
208
+ }
209
+
210
+ func (p * Process ) signal (sig os.Signal ) error {
211
+ Logger .Debugf ("Sending signal: %s to PID: %d" , sig .String (), p .Pid )
212
+
213
+ err := p .command .Process .Signal (syscall .SIGTERM )
214
+ if err != nil {
215
+ Logger .Errorf ("Failed to send signal: %s to PID: %d (%T: %v)" , sig .String (), p .Pid , err , err )
216
+ return err
217
+ }
218
+
219
+ return nil
220
+ }
221
+
222
+ // https://github.com/hnakamur/commango/blob/fe42b1cf82bf536ce7e24dceaef6656002e03743/os/executil/executil.go#L29
223
+ // TODO: Can this be better?
224
+ func getExitStatus (waitResult error ) string {
225
+ exitStatus := - 1
226
+
227
+ if waitResult != nil {
228
+ if err , ok := waitResult .(* exec.ExitError ); ok {
229
+ if s , ok := err .Sys ().(syscall.WaitStatus ); ok {
230
+ exitStatus = s .ExitStatus ()
231
+ } else {
232
+ Logger .Error ("Unimplemented for system where exec.ExitError.Sys() is not syscall.WaitStatus." )
233
+ }
135
234
}
136
235
} else {
137
- process . ExitStatus = "0"
236
+ exitStatus = 0
138
237
}
139
238
140
- Logger .Debugf ("Process with PID: %d finished with Exit Status: %s" , process .Pid , process .ExitStatus )
239
+ return fmt .Sprintf ("%d" , exitStatus )
240
+ }
141
241
242
+ func timeoutWait (waitGroup * sync.WaitGroup ) error {
142
243
// Make a chanel that we'll use as a timeout
143
244
c := make (chan int , 1 )
144
245
145
246
// Start waiting for the routines to finish
146
- Logger .Debug ("Waiting for io.Copy and incremental output to finish" )
147
247
go func () {
148
- w .Wait ()
248
+ waitGroup .Wait ()
149
249
c <- 1
150
250
}()
151
251
152
- // Sometimes (in docker containers) io.Copy never seems to finish. This is a mega
153
- // hack around it. If it doesn't finish after 1 second, just continue.
154
- // TODO: Whyyyyy!?!?!?
155
252
select {
156
253
case _ = <- c :
157
- // nothing, wait finished fine.
158
- case <- time .After (1 * time .Second ):
159
- Logger . Error ( "Timed out waiting for the routines to finish. Forcefully moving on. " )
254
+ return nil
255
+ case <- time .After (3 * time .Second ):
256
+ return errors . New ( "Timeout " )
160
257
}
161
258
162
- // Copy the final output back to the process
163
- process .Output = buffer .String ()
164
-
165
- // No error occured so we can return nil
166
- return & process , nil
259
+ return nil
167
260
}
0 commit comments