-
Notifications
You must be signed in to change notification settings - Fork 173
/
Copy pathcosash.go
193 lines (175 loc) · 4.91 KB
/
cosash.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
// Package cosash implements a "co-processing" proxy that is primarily
// designed to expose a Go API that is currently implemented by `src/cmdlib.sh`.
// A lot of the code in that file is stateful - e.g. APIs set environment variables
// and allocate temporary directories. So it wouldn't work very well to fork
// a new shell process each time.
//
// The "co-processing" here is a way to describe that there's intended to be
// a one-to-one relationship of the child bash process and the current one,
// although this is not strictly required. The Go APIs here call dynamically
// into the bash process by writing to its stdin, and can receive serialized
// data back over a pipe on file descriptor 3.
package cosash
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"github.com/coreos/coreos-assembler/internal/pkg/bashexec"
)
// CosaSh is a companion shell process which accepts commands
// piped over stdin.
type CosaSh struct {
cmd *exec.Cmd
input io.WriteCloser
preparedBuild bool
ackserial uint64
replychan <-chan (string)
errchan <-chan (error)
}
func parseAck(r *bufio.Reader, expected uint64) (string, error) {
linebytes, _, err := r.ReadLine()
if err != nil {
return "", err
}
line := string(linebytes)
parts := strings.SplitN(line, " ", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid reply from cosash: %s", line)
}
serial, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return "", fmt.Errorf("invalid reply from cosash: %s", line)
}
if serial != expected {
return "", fmt.Errorf("unexpected ack serial from cosash; expected=%d reply=%d", expected, serial)
}
return parts[1], nil
}
// NewCosaSh creates a new companion shell process
func NewCosaSh() (*CosaSh, error) {
cmd := exec.Command("/bin/bash")
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
}
// This is the channel where we send our commands
input, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
// stdout and stderr are the same as ours; we are effectively
// "co-processing", so we want to get output/errors as they're
// printed.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmdin, cmdout, err := os.Pipe()
if err != nil {
return nil, err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, cmdout)
// Start the process
if err := cmd.Start(); err != nil {
return nil, err
}
replychan := make(chan string)
errchan := make(chan error)
r := &CosaSh{
input: input,
cmd: cmd,
replychan: replychan,
errchan: errchan,
preparedBuild: false,
}
// Send a message when the process exits
go func() {
errchan <- cmd.Wait()
}()
// Parse the ack serials into a channel
go func() {
bufr := bufio.NewReader(cmdin)
for {
reply, err := parseAck(bufr, r.ackserial)
if err != nil {
// Don't propagate EOF, since we want the process exit status instead.
if err == io.EOF {
break
}
errchan <- err
break
}
r.ackserial += 1
replychan <- reply
}
}()
// Initialize the internal library
err = r.Process(fmt.Sprintf("%s\n. /usr/lib/coreos-assembler/cmdlib.sh\n", bashexec.StrictMode))
if err != nil {
return nil, fmt.Errorf("failed to init cosash: %w", err)
}
return r, nil
}
// write sends content to the shell's stdin, synchronously wait for the reply
func (r *CosaSh) ProcessWithReply(buf string) (string, error) {
// Inject code which writes the serial reply prefix
cmd := fmt.Sprintf("echo -n \"%d \" >&3\n", r.ackserial)
if _, err := io.WriteString(r.input, cmd); err != nil {
return "", err
}
// Tell the shell to execute the code, which should write the reply to fd 3
// which will complete the command.
if _, err := io.WriteString(r.input, buf); err != nil {
return "", err
}
if !strings.HasSuffix(buf, "\n") {
if _, err := io.WriteString(r.input, "\n"); err != nil {
return "", err
}
}
select {
case reply := <-r.replychan:
return reply, nil
case err := <-r.errchan:
return "", err
}
}
func (sh *CosaSh) Process(buf string) error {
buf = fmt.Sprintf("%s\necho OK >&3\n", buf)
r, err := sh.ProcessWithReply(buf)
if err != nil {
return err
}
if r != "OK" {
return fmt.Errorf("unexpected reply from cosash; expected OK, found %s", r)
}
return nil
}
// PrepareBuild prepares for a build, returning the newly allocated build directory
func (sh *CosaSh) PrepareBuild(artifact_name string) (string, error) {
if artifact_name != "" {
sh.Process(fmt.Sprintf("IMAGE_TYPE=%s", artifact_name))
}
return sh.ProcessWithReply(`prepare_build
pwd >&3
`)
}
// BaseArch returns the base architecture
func (sh *CosaSh) BaseArch() (string, error) {
return sh.ProcessWithReply(`echo $basearch >&3`)
}
// HasPrivileges checks if we can use sudo
func (sh *CosaSh) HasPrivileges() (bool, error) {
r, err := sh.ProcessWithReply(`
if has_privileges; then
echo true >&3
else
echo false >&3
fi`)
if err != nil {
return false, err
}
return strconv.ParseBool(r)
}