blob: 230e5d0c18e362e1496864a1daf9933f9707694c [file] [log] [blame]
Jiri Simsad7616c92015-03-24 23:44:30 -07001// Copyright 2015 The Vanadium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Jiri Simsa5293dcb2014-05-10 09:56:38 -07005package exec
6
7import (
Bogdan Caprita66ca3532015-02-05 21:08:10 -08008 "bytes"
Jiri Simsac199bc12014-05-30 12:52:24 -07009 "encoding/binary"
Cosmos Nicolaou1c18c1c2014-10-08 16:37:10 -070010 "fmt"
Jiri Simsac199bc12014-05-30 12:52:24 -070011 "io"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070012 "os"
13 "os/exec"
Robert Kroegerb5d6bda2015-01-30 16:10:49 -080014 "strconv"
Cosmos Nicolaou1c18c1c2014-10-08 16:37:10 -070015 "strings"
Bogdan Caprita650b1622014-11-21 15:11:05 -080016 "sync"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070017 "syscall"
18 "time"
Cosmos Nicolaoubfcac5f2014-05-22 21:57:35 -070019
Mike Burrowsccca2f42015-03-27 13:57:29 -070020 "v.io/v23/verror"
Todd Wang3bb46f52015-05-14 22:01:18 -070021 "v.io/x/lib/envvar"
Jiri Simsa337af232015-02-27 14:36:46 -080022 "v.io/x/lib/vlog"
Jiri Simsaffceefa2015-02-28 11:03:34 -080023 "v.io/x/ref/lib/timekeeper"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070024)
25
Mike Burrowsccca2f42015-03-27 13:57:29 -070026const pkgPath = "v.io/x/ref/lib/exec"
27
Jiri Simsa5293dcb2014-05-10 09:56:38 -070028var (
Mike Burrowsccca2f42015-03-27 13:57:29 -070029 ErrAuthTimeout = verror.Register(pkgPath+".ErrAuthTimeout", verror.NoRetry, "{1:}{2:} timeout in auth handshake{:_}")
30 ErrTimeout = verror.Register(pkgPath+".ErrTimeout", verror.NoRetry, "{1:}{2:} timeout waiting for child{:_}")
31 ErrSecretTooLarge = verror.Register(pkgPath+".ErrSecretTooLarge", verror.NoRetry, "{1:}{2:} secret is too large{:_}")
32 ErrNotUsingProtocol = verror.Register(pkgPath+".ErrNotUsingProtocol", verror.NoRetry, "{1:}{2:} not using parent/child exec protocol{:_}")
33
34 errFailedStatus = verror.Register(pkgPath+".errFailedStatus", verror.NoRetry, "{1:}{2:} {_}")
35 errUnrecognizedStatus = verror.Register(pkgPath+".errUnrecognizedStatus", verror.NoRetry, "{1:}{2:} unrecognised status from subprocess{:_}")
36 errUnexpectedType = verror.Register(pkgPath+".errUnexpectedType", verror.NoRetry, "{1:}{2:} unexpected type {3}{:_}")
37 errNoSuchProcess = verror.Register(pkgPath+".errNoSuchProcess", verror.NoRetry, "{1:}{2:} no such process{:_}")
38 errPartialWrite = verror.Register(pkgPath+".errPartialWrite", verror.NoRetry, "{1:}{2:} partial write{:_}")
Jiri Simsa5293dcb2014-05-10 09:56:38 -070039)
40
41// A ParentHandle is the Parent process' means of managing a single child.
42type ParentHandle struct {
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -070043 c *exec.Cmd
Cosmos Nicolaou486d3492014-09-30 22:21:20 -070044 config Config
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -070045 protocol bool // true if we're using the parent/child protocol.
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -070046 secret string
47 statusRead *os.File
48 statusWrite *os.File
49 tk timekeeper.TimeKeeper
Bogdan Caprita650b1622014-11-21 15:11:05 -080050 waitDone bool
51 waitErr error
52 waitLock sync.Mutex
Robert Kroegerb5d6bda2015-01-30 16:10:49 -080053 callbackPid int
Jiri Simsa5293dcb2014-05-10 09:56:38 -070054}
55
56// ParentHandleOpt is an option for NewParentHandle.
57type ParentHandleOpt interface {
Jiri Simsac199bc12014-05-30 12:52:24 -070058 // ExecParentHandleOpt is a signature 'dummy' method for the
59 // interface.
Jiri Simsa5293dcb2014-05-10 09:56:38 -070060 ExecParentHandleOpt()
61}
62
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070063// ConfigOpt can be used to seed the parent handle with a
64// config to be passed to the child.
65type ConfigOpt struct {
Cosmos Nicolaou486d3492014-09-30 22:21:20 -070066 Config
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070067}
Jiri Simsac199bc12014-05-30 12:52:24 -070068
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070069// ExecParentHandleOpt makes ConfigOpt an instance of
Jiri Simsac199bc12014-05-30 12:52:24 -070070// ParentHandleOpt.
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070071func (ConfigOpt) ExecParentHandleOpt() {}
Jiri Simsac199bc12014-05-30 12:52:24 -070072
73// SecretOpt can be used to seed the parent handle with a custom secret.
74type SecretOpt string
75
76// ExecParentHandleOpt makes SecretOpt an instance of ParentHandleOpt.
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070077func (SecretOpt) ExecParentHandleOpt() {}
Jiri Simsac199bc12014-05-30 12:52:24 -070078
Jiri Simsa5293dcb2014-05-10 09:56:38 -070079// TimeKeeperOpt can be used to seed the parent handle with a custom timekeeper.
80type TimeKeeperOpt struct {
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -070081 timekeeper.TimeKeeper
Jiri Simsa5293dcb2014-05-10 09:56:38 -070082}
83
84// ExecParentHandleOpt makes TimeKeeperOpt an instance of ParentHandleOpt.
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -070085func (TimeKeeperOpt) ExecParentHandleOpt() {}
Jiri Simsa5293dcb2014-05-10 09:56:38 -070086
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -070087// UseExecProtocolOpt can be used to control whether parent/child handshake
88// protocol is used. WaitForReady will return immediately with an error if
Cosmos Nicolaou52e9a222015-03-16 21:57:16 -070089// this option is set to false. The defaults behaviour is to assume that
90// the exec protocol is used. If it is not used, then the Start method
91// will not create shared file descriptors to use for the exec protocol.
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -070092type UseExecProtocolOpt bool
93
94func (UseExecProtocolOpt) ExecParentHandleOpt() {}
95
Jiri Simsa5293dcb2014-05-10 09:56:38 -070096// NewParentHandle creates a ParentHandle for the child process represented by
97// an instance of exec.Cmd.
Jiri Simsac199bc12014-05-30 12:52:24 -070098func NewParentHandle(c *exec.Cmd, opts ...ParentHandleOpt) *ParentHandle {
Cosmos Nicolaou486d3492014-09-30 22:21:20 -070099 cfg, secret := NewConfig(), ""
Jiri Simsac199bc12014-05-30 12:52:24 -0700100 tk := timekeeper.RealTime()
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700101 protocol := true
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700102 for _, opt := range opts {
103 switch v := opt.(type) {
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -0700104 case ConfigOpt:
105 cfg = v
Jiri Simsac199bc12014-05-30 12:52:24 -0700106 case SecretOpt:
107 secret = string(v)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700108 case TimeKeeperOpt:
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -0700109 tk = v
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700110 case UseExecProtocolOpt:
111 protocol = bool(v)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700112 default:
113 vlog.Errorf("Unrecognized parent option: %v", v)
114 }
115 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700116 return &ParentHandle{
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700117 c: c,
118 protocol: protocol,
119 config: cfg,
120 secret: secret,
121 tk: tk,
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700122 }
123}
124
125// Start starts the child process, sharing a secret with it and
126// setting up a communication channel over which to read its status.
127func (p *ParentHandle) Start() error {
Todd Wang3bb46f52015-05-14 22:01:18 -0700128 env := envvar.SliceToMap(p.c.Env)
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700129 if !p.protocol {
Todd Wang3bb46f52015-05-14 22:01:18 -0700130 // Ensure ExecVersionVariable is not set if we're not using the protocol.
131 delete(env, ExecVersionVariable)
132 p.c.Env = envvar.MapToSlice(env)
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700133 return p.c.Start()
134 }
Todd Wang3bb46f52015-05-14 22:01:18 -0700135 // Ensure ExecVersionVariable is always set if we are using the protocol.
136 env[ExecVersionVariable] = version1
137 p.c.Env = envvar.MapToSlice(env)
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700138
Jiri Simsac199bc12014-05-30 12:52:24 -0700139 // Create anonymous pipe for communicating data between the child
140 // and the parent.
Bogdan Caprita7f491672014-11-13 14:51:08 -0800141 // TODO(caprita): As per ribrdb@, Go's exec does not prune the set
142 // of file descriptors passed down to the child process, and hence
143 // a child may get access to the files meant for another child.
144 // Do we need to ensure only one thread is allowed to create these
145 // pipes at any time?
Jiri Simsac199bc12014-05-30 12:52:24 -0700146 dataRead, dataWrite, err := os.Pipe()
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700147 if err != nil {
148 return err
149 }
Jiri Simsac199bc12014-05-30 12:52:24 -0700150 defer dataRead.Close()
151 defer dataWrite.Close()
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700152 statusRead, statusWrite, err := os.Pipe()
153 if err != nil {
154 return err
155 }
156 p.statusRead = statusRead
157 p.statusWrite = statusWrite
Jiri Simsac199bc12014-05-30 12:52:24 -0700158 // Add the parent-child pipes to cmd.ExtraFiles, offsetting all
159 // existing file descriptors accordingly.
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700160 extraFiles := make([]*os.File, len(p.c.ExtraFiles)+2)
Jiri Simsac199bc12014-05-30 12:52:24 -0700161 extraFiles[0] = dataRead
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700162 extraFiles[1] = statusWrite
163 for i, _ := range p.c.ExtraFiles {
164 extraFiles[i+2] = p.c.ExtraFiles[i]
165 }
166 p.c.ExtraFiles = extraFiles
Jiri Simsac199bc12014-05-30 12:52:24 -0700167 // Start the child process.
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700168 if err := p.c.Start(); err != nil {
169 p.statusWrite.Close()
170 p.statusRead.Close()
171 return err
172 }
Jiri Simsac199bc12014-05-30 12:52:24 -0700173 // Pass data to the child using a pipe.
Bogdan Capritaa4d9ee42014-06-20 16:42:53 -0700174 serializedConfig, err := p.config.Serialize()
175 if err != nil {
176 return err
177 }
178 if err := encodeString(dataWrite, serializedConfig); err != nil {
Jiri Simsac199bc12014-05-30 12:52:24 -0700179 p.statusWrite.Close()
180 p.statusRead.Close()
181 return err
182 }
Jiri Simsa84059da2014-06-02 17:22:05 -0700183 if err := encodeString(dataWrite, p.secret); err != nil {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700184 p.statusWrite.Close()
185 p.statusRead.Close()
186 return err
187 }
188 return nil
189}
190
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800191// copy is like io.Copy, but it also treats the receipt of the special eofChar
192// byte to mean io.EOF.
193func copy(w io.Writer, r io.Reader) (err error) {
194 buf := make([]byte, 1024)
195 for {
196 nRead, errRead := r.Read(buf)
197 if nRead > 0 {
198 if eofCharIndex := bytes.IndexByte(buf[:nRead], eofChar); eofCharIndex != -1 {
199 nRead = eofCharIndex
200 errRead = io.EOF
201 }
202 nWrite, errWrite := w.Write(buf[:nRead])
203 if errWrite != nil {
204 err = errWrite
205 break
206 }
207 if nRead != nWrite {
208 err = io.ErrShortWrite
209 break
210 }
211 }
212 if errRead == io.EOF {
213 break
214 }
215 if errRead != nil {
216 err = errRead
217 break
218 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700219 }
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800220 return
221}
222
223func waitForStatus(c chan interface{}, r *os.File) {
224 var readBytes bytes.Buffer
225 err := copy(&readBytes, r)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700226 r.Close()
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800227 if err != nil {
228 c <- err
229 } else {
230 c <- readBytes.String()
231 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700232 close(c)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700233}
234
235// WaitForReady will wait for the child process to become ready.
236func (p *ParentHandle) WaitForReady(timeout time.Duration) error {
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700237 if !p.protocol {
Mike Burrowsccca2f42015-03-27 13:57:29 -0700238 return verror.New(ErrNotUsingProtocol, nil)
Cosmos Nicolaoub6cd3962015-03-08 22:57:03 -0700239 }
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800240 // An invariant of WaitForReady is that both statusWrite and statusRead
241 // get closed before WaitForStatus returns (statusRead gets closed by
242 // waitForStatus).
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700243 defer p.statusWrite.Close()
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800244 c := make(chan interface{}, 1)
245 go waitForStatus(c, p.statusRead)
246 // TODO(caprita): This can be simplified further by doing the reading
247 // from the status pipe here, and instead moving the timeout listener to
248 // a separate goroutine.
249 select {
250 case msg := <-c:
251 switch m := msg.(type) {
252 case error:
253 return m
254 case string:
255 if strings.HasPrefix(m, readyStatus) {
256 pid, err := strconv.Atoi(m[len(readyStatus):])
Robert Kroegerb5d6bda2015-01-30 16:10:49 -0800257 if err != nil {
258 return err
259 }
260 p.callbackPid = pid
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700261 return nil
262 }
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800263 if strings.HasPrefix(m, failedStatus) {
Mike Burrowsccca2f42015-03-27 13:57:29 -0700264 return verror.New(errFailedStatus, nil, strings.TrimPrefix(m, failedStatus))
Cosmos Nicolaou1c18c1c2014-10-08 16:37:10 -0700265 }
Mike Burrowsccca2f42015-03-27 13:57:29 -0700266 return verror.New(errUnrecognizedStatus, nil, m)
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800267 default:
Mike Burrowsccca2f42015-03-27 13:57:29 -0700268 return verror.New(errUnexpectedType, nil, fmt.Sprintf("%T", m))
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700269 }
Bogdan Caprita66ca3532015-02-05 21:08:10 -0800270 case <-p.tk.After(timeout):
271 vlog.Errorf("Timed out waiting for child status")
272 // By writing the special eofChar byte to the pipe, we ensure
273 // that waitForStatus returns: the copy function treats eofChar
274 // to indicate end of read input. Note, copy could have
275 // finished for other reasons already (receipt of eofChar from
276 // the child process). Note, closing the pipe from the child
277 // (explicitly or due to crash) would NOT cause copy to read
278 // io.EOF, since we keep the statusWrite open in the parent.
279 // Hence, a child crash will eventually trigger this timeout.
280 p.statusWrite.Write([]byte{eofChar})
281 // Before returning, waitForStatus will close r, and then close
282 // c. Waiting on c ensures that r.Close() in waitForStatus
283 // already executed.
284 <-c
Mike Burrowsccca2f42015-03-27 13:57:29 -0700285 return verror.New(ErrTimeout, nil)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700286 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700287}
288
Bogdan Caprita650b1622014-11-21 15:11:05 -0800289// wait performs the Wait on the underlying command under lock, and only once
290// (subsequent wait calls block until the Wait is finished). It's ok to call
291// wait multiple times, and in parallel. The error from the initial Wait is
292// cached and returned for all subsequent calls.
293func (p *ParentHandle) wait() error {
294 p.waitLock.Lock()
295 defer p.waitLock.Unlock()
296 if p.waitDone {
297 return p.waitErr
298 }
299 p.waitErr = p.c.Wait()
300 p.waitDone = true
301 return p.waitErr
302}
303
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700304// Wait will wait for the child process to terminate of its own accord.
305// It returns nil if the process exited cleanly with an exit status of 0,
306// any other exit code or error will result in an appropriate error return
307func (p *ParentHandle) Wait(timeout time.Duration) error {
308 c := make(chan error, 1)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700309 go func() {
Bogdan Caprita650b1622014-11-21 15:11:05 -0800310 c <- p.wait()
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700311 close(c)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700312 }()
313 // If timeout is zero time.After will panic; we handle zero specially
314 // to mean infinite timeout.
315 if timeout > 0 {
316 select {
317 case <-p.tk.After(timeout):
Mike Burrowsccca2f42015-03-27 13:57:29 -0700318 return verror.New(ErrTimeout, nil)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700319 case err := <-c:
320 return err
321 }
322 } else {
323 return <-c
324 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700325}
326
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -0700327// Pid returns the pid of the child, 0 if the child process doesn't exist
328func (p *ParentHandle) Pid() int {
329 if p.c.Process != nil {
330 return p.c.Process.Pid
331 }
332 return 0
333}
334
Robert Kroegerb5d6bda2015-01-30 16:10:49 -0800335// ChildPid returns the pid of a child process as reported by its status
336// callback.
337func (p *ParentHandle) ChildPid() int {
338 return p.callbackPid
339}
340
Cosmos Nicolaouee7abc22014-05-27 10:50:03 -0700341// Exists returns true if the child process exists and can be signal'ed
342func (p *ParentHandle) Exists() bool {
343 if p.c.Process != nil {
344 return syscall.Kill(p.c.Process.Pid, 0) == nil
345 }
346 return false
347}
348
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700349// Kill kills the child process.
350func (p *ParentHandle) Kill() error {
Robin Thellend44903522015-02-06 12:55:26 -0800351 if p.c.Process == nil {
Mike Burrowsccca2f42015-03-27 13:57:29 -0700352 return verror.New(errNoSuchProcess, nil)
Robin Thellend44903522015-02-06 12:55:26 -0800353 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700354 return p.c.Process.Kill()
355}
356
357// Signal sends the given signal to the child process.
358func (p *ParentHandle) Signal(sig syscall.Signal) error {
Robin Thellend44903522015-02-06 12:55:26 -0800359 if p.c.Process == nil {
Mike Burrowsccca2f42015-03-27 13:57:29 -0700360 return verror.New(errNoSuchProcess, nil)
Robin Thellend44903522015-02-06 12:55:26 -0800361 }
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700362 return syscall.Kill(p.c.Process.Pid, sig)
363}
364
365// Clean will clean up state, including killing the child process.
366func (p *ParentHandle) Clean() error {
367 if err := p.Kill(); err != nil {
368 return err
369 }
Bogdan Caprita650b1622014-11-21 15:11:05 -0800370 return p.wait()
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700371}
Jiri Simsac199bc12014-05-30 12:52:24 -0700372
Jiri Simsa84059da2014-06-02 17:22:05 -0700373func encodeString(w io.Writer, data string) error {
Jiri Simsac199bc12014-05-30 12:52:24 -0700374 l := len(data)
375 if err := binary.Write(w, binary.BigEndian, int64(l)); err != nil {
376 return err
377 }
378 if n, err := w.Write([]byte(data)); err != nil || n != l {
379 if err != nil {
380 return err
381 } else {
Mike Burrowsccca2f42015-03-27 13:57:29 -0700382 return verror.New(errPartialWrite, nil)
Jiri Simsac199bc12014-05-30 12:52:24 -0700383 }
384 }
385 return nil
386}