forked from markriggins/dockerfy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exec.go
133 lines (120 loc) · 3.59 KB
/
exec.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
package main
import (
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"golang.org/x/net/context"
)
func runCmd(ctx context.Context, cancel context.CancelFunc, cmd *exec.Cmd, cancel_when_finished bool) {
defer wg.Done()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := copySecretsFiles(cmd); err != nil {
log.Fatalf("Could not copy secrets files", err)
}
for i, arg := range cmd.Args {
cmd.Args[i] = string_template_eval(arg)
}
// start the cmd
err := cmd.Start()
if err != nil {
// TODO: bubble the platform-specific exit code of the process up via global exitCode
log.Fatalf("Error starting command: `%s` - %s\n", toString(cmd), err)
}
if debugFlag && cmd.SysProcAttr != nil && cmd.SysProcAttr.Credential != nil {
log.Printf("command running as uid %d", cmd.SysProcAttr.Credential.Uid)
}
// Setup signaling -- a separate channel for goroutine for each command
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL)
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case sig, ok := <-sigs:
if !ok {
return
}
if debugFlag {
if sig != nil {
log.Printf("Command `%s` received signal %s", toString(cmd), sig)
} else {
log.Printf("Command `%s` done waiting for signals", toString(cmd))
return
}
}
if sig == nil {
signalProcessWithTimeout(cmd, syscall.SIGTERM)
signalProcessWithTimeout(cmd, syscall.SIGKILL)
return
} else {
// Pass signals thru to children, let them decide how to handle it.
signalProcessWithTimeout(cmd, sig)
}
case <-ctx.Done():
if debugFlag {
log.Printf("Command `%s` done waiting for signals (ctx.Done())", toString(cmd))
}
signalProcessWithTimeout(cmd, syscall.SIGTERM)
signalProcessWithTimeout(cmd, syscall.SIGKILL)
return
}
}
}()
err = cmd.Wait()
signal.Stop(sigs)
close(sigs)
if err == nil {
if verboseFlag {
log.Printf("Command finished successfully: `%s`\n", toString(cmd))
}
if cancel_when_finished {
cancel()
}
} else {
log.Printf("Command `%s` exited with error: %s\n", toString(cmd), err)
if exitCode == 0 {
// First child to exit with an error sets the exitCode
if exiterr, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitCode = waitStatus.ExitStatus()
if verboseFlag {
log.Printf("\tand exit_code %d", exitCode)
}
} else {
log.Printf("Cloud not determine WaitStatus")
}
} else {
log.Printf("Could not determine ExitError")
}
// If platform-specific exit_code cannot be determined exit with
// with generic 1 for failure
if exitCode == 0 {
exitCode = 1
}
}
cancel()
// OPTIMIZE: This could be cleaner
// os.Exit(err.(*exec.ExitError).Sys().(syscall.WaitStatus).ExitStatus())
}
}
func signalProcessWithTimeout(cmd *exec.Cmd, sig os.Signal) {
done := make(chan struct{})
go func() {
cmd.Process.Signal(sig)
cmd.Wait()
close(done)
}()
select {
case <-done:
return
case <-time.After(10 * time.Second):
log.Println("Killing command due to timeout.")
cmd.Process.Kill()
}
}