This repository has been archived by the owner on Jan 25, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
/
iodaemon.go
131 lines (107 loc) · 2.53 KB
/
iodaemon.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
package iodaemon
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"sync"
"syscall"
"time"
"io"
)
// spawn listens on a unix socket at the given socketPath and when the first connection
// is received, starts a child process.
func Spawn(
socketPath string,
argv []string,
timeout time.Duration,
notifyStream io.WriteCloser,
wirer *Wirer,
daemon *Daemon,
) error {
listener, err := listen(socketPath)
if err != nil {
return err
}
defer listener.Close()
executablePath, err := exec.LookPath(argv[0])
if err != nil {
return fmt.Errorf("executable %s not found: %s", argv[0], err)
}
cmd := child(executablePath, argv)
stdinW, stdoutR, stderrR, extraFdW, err := wirer.Wire(cmd)
if err != nil {
return err
}
statusR, statusW, err := os.Pipe()
if err != nil {
return err
}
launched := make(chan bool)
errChan := make(chan error)
go func() {
var once sync.Once
for {
fmt.Fprintln(notifyStream, "ready")
conn, err := acceptConnection(listener, stdoutR, stderrR, statusR)
if err != nil {
errChan <- err
return // in general this means the listener has been closed
}
once.Do(func() {
err := cmd.Start()
if err != nil {
errChan <- fmt.Errorf("executable %s failed to start: %s", executablePath, err)
return
}
fmt.Fprintln(notifyStream, "active")
notifyStream.Close()
launched <- true
})
daemon.HandleConnection(conn, cmd.Process, stdinW, extraFdW)
}
}()
select {
case err := <-errChan:
return err
case <-launched:
var exit byte = 0
if err := cmd.Wait(); err != nil {
ws := err.(*exec.ExitError).ProcessState.Sys().(syscall.WaitStatus)
exit = byte(ws.ExitStatus())
}
fmt.Fprintf(statusW, "%d\n", exit)
case <-time.After(timeout):
return fmt.Errorf("expected client to connect within %s", timeout)
}
return nil
}
func listen(socketPath string) (net.Listener, error) {
// Delete socketPath if it exists to avoid bind failures.
err := os.Remove(socketPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
err = os.MkdirAll(filepath.Dir(socketPath), 0755)
if err != nil {
return nil, err
}
return net.Listen("unix", socketPath)
}
func acceptConnection(listener net.Listener, stdoutR, stderrR, statusR *os.File) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
}
rights := syscall.UnixRights(
int(stdoutR.Fd()),
int(stderrR.Fd()),
int(statusR.Fd()),
)
_, _, err = conn.(*net.UnixConn).WriteMsgUnix([]byte{}, rights, nil)
if err != nil {
return nil, err
}
return conn, nil
}