forked from warpfork/pogo
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathexec_proc.go
286 lines (251 loc) · 7.83 KB
/
exec_proc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package gosh
import (
"fmt"
"os"
"os/exec"
"sync"
"sync/atomic"
"syscall"
"time"
)
var _ Proc = &ExecProc{}
/*
`gosh.Proc` implementation using `os/exec`.
*/
type ExecProc struct {
/*
Guards all major transitions... *including* to `state`.
`state` is safe to access by fenced read, but all changes to it are
under this mutex (because there's other matching checks and changes
going on with every state transition anyway).
*/
mutex sync.Mutex
/*
Always access this with functions from the atomic package, and when
transitioning states set the status after all other fields are mutated,
so that checks of State() serve as a memory barrier for all.
This is actually a `gosh.State`, but `atomic` functions don't understand
typedefs, so we keep it as an int and coerce it whenever we expose it.
*/
state int32
cmd *exec.Cmd
/* If set, a major error (i.e. status=PANICKED; does not include nonzero exit statuses). */
err error
/* Wait for this to close in order to wait for the process to return. */
exitCh chan struct{}
/*
Exit code if we're state==FINISHED and exit codes are possible on this platform, or
-1 if we're not there yet. Will not change after exitCh has closed.
*/
exitCode int
/* Functions to call back when the command has exited. */
exitListeners []func(Proc)
}
func ExecProcCmd(cmd *exec.Cmd) Proc {
p := &ExecProc{
cmd: cmd,
state: int32(UNSTARTED),
exitCh: make(chan struct{}),
exitCode: -1,
}
if err := p.start(); err != nil {
panic(err)
}
return p
}
func (p *ExecProc) State() State {
return State(atomic.LoadInt32(&p.state))
}
func (p *ExecProc) Pid() int {
if p.State().IsStarted() {
return p.cmd.Process.Pid
} else {
return -1
}
}
func (p *ExecProc) WaitChan() <-chan struct{} {
return p.exitCh
}
func (p *ExecProc) Wait() {
<-p.WaitChan()
}
func (p *ExecProc) WaitSoon(d time.Duration) bool {
select {
case <-time.After(d):
return false
case <-p.WaitChan():
return true
}
}
func (p *ExecProc) GetExitCode() int {
if !p.State().IsDone() {
p.Wait()
}
return p.exitCode
}
func (p *ExecProc) GetExitCodeSoon(d time.Duration) int {
if p.WaitSoon(d) {
return p.exitCode
} else {
return -1
}
}
func (p *ExecProc) AddExitListener(callback func(Proc)) {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.State().IsDone() {
// TODO: a better standard of panic handling here
callback(p)
} else {
p.exitListeners = append(p.exitListeners, callback)
}
}
func (p *ExecProc) Kill() {
err := p.cmd.Process.Kill()
if err != nil {
panic(ProcMonitorError{err})
}
}
func (p *ExecProc) Signal(sig os.Signal) {
err := p.cmd.Process.Signal(sig)
if err != nil {
panic(ProcMonitorError{err})
}
}
//
// Below lieth Guts
//
func (p *ExecProc) start() error {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.State().IsStarted() {
return nil
}
atomic.StoreInt32(&p.state, int32(RUNNING))
if err := p.cmd.Start(); err != nil {
// These checks are such an eldrich horror *they can't even fit
// into a single switch statement*, because the go standard library
// cannot decide between "value" and "typed" errors, so here we
// go with both a type switch inside a typeswitch AND a set of
// pointer equality checks inside a typeswitch, and then just for
// good measure some string compares (because otherwise you can't
// tell the difference between the workingdir not existing and the
// command path not existing).
//
// Mercy.
//
switch err2 := err.(type) {
case *exec.Error:
switch err2.Err {
case exec.ErrNotFound, os.ErrPermission:
// and yes, grepping the exec package indicates `os.ErrPermission` only occurs in specifically this case in the search for the executable file.
// specifically, you might worry that it's ambiguous with, say, execing with a chroot and getting perm denied; it is not.
p.transitionFinal(NoSuchCommandError{
Name: p.cmd.Args[0],
Cause: err,
})
return p.err
}
case *os.PathError:
switch err2.Op {
case "fork/exec":
switch err2.Err {
case syscall.ENOENT:
p.transitionFinal(NoSuchCommandError{
Name: p.cmd.Args[0],
Cause: err,
})
return p.err
default:
// if no special recognition,
// fall out to general ProcMonitorError.
// e.g. EPERM falls out here; we're not out to replace all stdlib errors,
// just make sure they're all wrapped and clearly indicated as emitted from gosh,
// and normalize the ones that are really wonky like (namely, no such command coming from 2~3 radically different branches).
}
case "chdir":
p.transitionFinal(NoSuchCwdError{
Path: p.cmd.Dir,
Cause: err,
})
return p.err
}
}
p.transitionFinal(ProcMonitorError{Cause: err})
return p.err
}
go p.waitAndHandleExit()
return nil
}
func (p *ExecProc) waitAndHandleExit() {
exitCode := -1
var err error
for err == nil && exitCode == -1 {
exitCode, err = p.waitTry()
}
// Do one last Wait for good ol' times sake. And to use the Cmd.closeDescriptors feature.
p.cmd.Wait()
p.mutex.Lock()
defer p.mutex.Unlock()
p.exitCode = exitCode
p.transitionFinal(err)
}
func (p *ExecProc) waitTry() (int, error) {
// The docs for os.Process.Wait() state "Wait waits for the Process to exit".
// IT LIES.
//
// On unixy systems, under some states, os.Process.Wait() *also* returns for signals and other state changes. See comments below, where waitStatus is being checked.
// To actually wait for the process to exit, you have to Wait() repeatedly and check if the system-dependent codes are representative of real exit.
//
// You can *not* use os/exec.Cmd.Wait() to reliably wait for a command to exit on unix. Can. Not. Do it.
// os/exec.Cmd.Wait() explicitly sets a flag to see if you've called it before, and tells you to go to hell if you have.
// Since Cmd.Wait() uses Process.Wait(), the latter of which cannot function correctly without repeated calls, and the former of which forbids repeated calls...
// Yep, it's literally impossible to use os/exec.Cmd.Wait() correctly on unix.
//
processState, err := p.cmd.Process.Wait()
if err != nil {
return -1, err
}
if waitStatus, ok := processState.Sys().(syscall.WaitStatus); ok {
if waitStatus.Exited() {
return waitStatus.ExitStatus(), nil
} else if waitStatus.Signaled() {
// In bash, when a processs ends from a signal, the $? variable is set to 128+SIG.
// We follow that same convention here.
// So, a process terminated by ctrl-C returns 130. A script that died to kill-9 returns 137.
return int(waitStatus.Signal()) + 128, nil
} else {
// This should be more or less unreachable.
// ... the operative word there being "should". Read: "you wish".
// WaitStatus also defines Continued and Stopped states, but in practice, they don't (typically) appear here,
// because deep down, syscall.Wait4 is being called with options=0, and getting those states would require
// syscall.Wait4 being called with WUNTRACED or WCONTINUED.
// However, syscall.Wait4 may also return the Continued and Stoppe states if ptrace() has been attached to the child,
// so, really, anything is possible here.
// And thus, we have to return a special code here that causes wait to be tried in a loop.
return -1, nil
}
} else {
panic(fmt.Errorf("gosh only works systems with posix-style process semantics."))
}
}
func (p *ExecProc) transitionFinal(err error) {
// must hold cmd.mutex before calling this
// golang is an epic troll: claims to be best buddy for concurrent code, SYNC PACKAGE DOES NOT HAVE REENTRANT LOCKS
if p.State().IsRunning() {
if err == nil {
atomic.StoreInt32(&p.state, int32(FINISHED))
} else {
p.err = err
atomic.StoreInt32(&p.state, int32(PANICKED))
}
// iterate over exit listeners
for _, cb := range p.exitListeners {
func() {
// TODO: a better standard of panic handling here
cb(p)
}()
}
}
close(p.exitCh)
}