blob: 230e5d0c18e362e1496864a1daf9933f9707694c [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package exec
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"syscall"
"time"
"v.io/v23/verror"
"v.io/x/lib/envvar"
"v.io/x/lib/vlog"
"v.io/x/ref/lib/timekeeper"
)
const pkgPath = "v.io/x/ref/lib/exec"
var (
ErrAuthTimeout = verror.Register(pkgPath+".ErrAuthTimeout", verror.NoRetry, "{1:}{2:} timeout in auth handshake{:_}")
ErrTimeout = verror.Register(pkgPath+".ErrTimeout", verror.NoRetry, "{1:}{2:} timeout waiting for child{:_}")
ErrSecretTooLarge = verror.Register(pkgPath+".ErrSecretTooLarge", verror.NoRetry, "{1:}{2:} secret is too large{:_}")
ErrNotUsingProtocol = verror.Register(pkgPath+".ErrNotUsingProtocol", verror.NoRetry, "{1:}{2:} not using parent/child exec protocol{:_}")
errFailedStatus = verror.Register(pkgPath+".errFailedStatus", verror.NoRetry, "{1:}{2:} {_}")
errUnrecognizedStatus = verror.Register(pkgPath+".errUnrecognizedStatus", verror.NoRetry, "{1:}{2:} unrecognised status from subprocess{:_}")
errUnexpectedType = verror.Register(pkgPath+".errUnexpectedType", verror.NoRetry, "{1:}{2:} unexpected type {3}{:_}")
errNoSuchProcess = verror.Register(pkgPath+".errNoSuchProcess", verror.NoRetry, "{1:}{2:} no such process{:_}")
errPartialWrite = verror.Register(pkgPath+".errPartialWrite", verror.NoRetry, "{1:}{2:} partial write{:_}")
)
// A ParentHandle is the Parent process' means of managing a single child.
type ParentHandle struct {
c *exec.Cmd
config Config
protocol bool // true if we're using the parent/child protocol.
secret string
statusRead *os.File
statusWrite *os.File
tk timekeeper.TimeKeeper
waitDone bool
waitErr error
waitLock sync.Mutex
callbackPid int
}
// ParentHandleOpt is an option for NewParentHandle.
type ParentHandleOpt interface {
// ExecParentHandleOpt is a signature 'dummy' method for the
// interface.
ExecParentHandleOpt()
}
// ConfigOpt can be used to seed the parent handle with a
// config to be passed to the child.
type ConfigOpt struct {
Config
}
// ExecParentHandleOpt makes ConfigOpt an instance of
// ParentHandleOpt.
func (ConfigOpt) ExecParentHandleOpt() {}
// SecretOpt can be used to seed the parent handle with a custom secret.
type SecretOpt string
// ExecParentHandleOpt makes SecretOpt an instance of ParentHandleOpt.
func (SecretOpt) ExecParentHandleOpt() {}
// TimeKeeperOpt can be used to seed the parent handle with a custom timekeeper.
type TimeKeeperOpt struct {
timekeeper.TimeKeeper
}
// ExecParentHandleOpt makes TimeKeeperOpt an instance of ParentHandleOpt.
func (TimeKeeperOpt) ExecParentHandleOpt() {}
// UseExecProtocolOpt can be used to control whether parent/child handshake
// protocol is used. WaitForReady will return immediately with an error if
// this option is set to false. The defaults behaviour is to assume that
// the exec protocol is used. If it is not used, then the Start method
// will not create shared file descriptors to use for the exec protocol.
type UseExecProtocolOpt bool
func (UseExecProtocolOpt) ExecParentHandleOpt() {}
// NewParentHandle creates a ParentHandle for the child process represented by
// an instance of exec.Cmd.
func NewParentHandle(c *exec.Cmd, opts ...ParentHandleOpt) *ParentHandle {
cfg, secret := NewConfig(), ""
tk := timekeeper.RealTime()
protocol := true
for _, opt := range opts {
switch v := opt.(type) {
case ConfigOpt:
cfg = v
case SecretOpt:
secret = string(v)
case TimeKeeperOpt:
tk = v
case UseExecProtocolOpt:
protocol = bool(v)
default:
vlog.Errorf("Unrecognized parent option: %v", v)
}
}
return &ParentHandle{
c: c,
protocol: protocol,
config: cfg,
secret: secret,
tk: tk,
}
}
// Start starts the child process, sharing a secret with it and
// setting up a communication channel over which to read its status.
func (p *ParentHandle) Start() error {
env := envvar.SliceToMap(p.c.Env)
if !p.protocol {
// Ensure ExecVersionVariable is not set if we're not using the protocol.
delete(env, ExecVersionVariable)
p.c.Env = envvar.MapToSlice(env)
return p.c.Start()
}
// Ensure ExecVersionVariable is always set if we are using the protocol.
env[ExecVersionVariable] = version1
p.c.Env = envvar.MapToSlice(env)
// Create anonymous pipe for communicating data between the child
// and the parent.
// TODO(caprita): As per ribrdb@, Go's exec does not prune the set
// of file descriptors passed down to the child process, and hence
// a child may get access to the files meant for another child.
// Do we need to ensure only one thread is allowed to create these
// pipes at any time?
dataRead, dataWrite, err := os.Pipe()
if err != nil {
return err
}
defer dataRead.Close()
defer dataWrite.Close()
statusRead, statusWrite, err := os.Pipe()
if err != nil {
return err
}
p.statusRead = statusRead
p.statusWrite = statusWrite
// Add the parent-child pipes to cmd.ExtraFiles, offsetting all
// existing file descriptors accordingly.
extraFiles := make([]*os.File, len(p.c.ExtraFiles)+2)
extraFiles[0] = dataRead
extraFiles[1] = statusWrite
for i, _ := range p.c.ExtraFiles {
extraFiles[i+2] = p.c.ExtraFiles[i]
}
p.c.ExtraFiles = extraFiles
// Start the child process.
if err := p.c.Start(); err != nil {
p.statusWrite.Close()
p.statusRead.Close()
return err
}
// Pass data to the child using a pipe.
serializedConfig, err := p.config.Serialize()
if err != nil {
return err
}
if err := encodeString(dataWrite, serializedConfig); err != nil {
p.statusWrite.Close()
p.statusRead.Close()
return err
}
if err := encodeString(dataWrite, p.secret); err != nil {
p.statusWrite.Close()
p.statusRead.Close()
return err
}
return nil
}
// copy is like io.Copy, but it also treats the receipt of the special eofChar
// byte to mean io.EOF.
func copy(w io.Writer, r io.Reader) (err error) {
buf := make([]byte, 1024)
for {
nRead, errRead := r.Read(buf)
if nRead > 0 {
if eofCharIndex := bytes.IndexByte(buf[:nRead], eofChar); eofCharIndex != -1 {
nRead = eofCharIndex
errRead = io.EOF
}
nWrite, errWrite := w.Write(buf[:nRead])
if errWrite != nil {
err = errWrite
break
}
if nRead != nWrite {
err = io.ErrShortWrite
break
}
}
if errRead == io.EOF {
break
}
if errRead != nil {
err = errRead
break
}
}
return
}
func waitForStatus(c chan interface{}, r *os.File) {
var readBytes bytes.Buffer
err := copy(&readBytes, r)
r.Close()
if err != nil {
c <- err
} else {
c <- readBytes.String()
}
close(c)
}
// WaitForReady will wait for the child process to become ready.
func (p *ParentHandle) WaitForReady(timeout time.Duration) error {
if !p.protocol {
return verror.New(ErrNotUsingProtocol, nil)
}
// An invariant of WaitForReady is that both statusWrite and statusRead
// get closed before WaitForStatus returns (statusRead gets closed by
// waitForStatus).
defer p.statusWrite.Close()
c := make(chan interface{}, 1)
go waitForStatus(c, p.statusRead)
// TODO(caprita): This can be simplified further by doing the reading
// from the status pipe here, and instead moving the timeout listener to
// a separate goroutine.
select {
case msg := <-c:
switch m := msg.(type) {
case error:
return m
case string:
if strings.HasPrefix(m, readyStatus) {
pid, err := strconv.Atoi(m[len(readyStatus):])
if err != nil {
return err
}
p.callbackPid = pid
return nil
}
if strings.HasPrefix(m, failedStatus) {
return verror.New(errFailedStatus, nil, strings.TrimPrefix(m, failedStatus))
}
return verror.New(errUnrecognizedStatus, nil, m)
default:
return verror.New(errUnexpectedType, nil, fmt.Sprintf("%T", m))
}
case <-p.tk.After(timeout):
vlog.Errorf("Timed out waiting for child status")
// By writing the special eofChar byte to the pipe, we ensure
// that waitForStatus returns: the copy function treats eofChar
// to indicate end of read input. Note, copy could have
// finished for other reasons already (receipt of eofChar from
// the child process). Note, closing the pipe from the child
// (explicitly or due to crash) would NOT cause copy to read
// io.EOF, since we keep the statusWrite open in the parent.
// Hence, a child crash will eventually trigger this timeout.
p.statusWrite.Write([]byte{eofChar})
// Before returning, waitForStatus will close r, and then close
// c. Waiting on c ensures that r.Close() in waitForStatus
// already executed.
<-c
return verror.New(ErrTimeout, nil)
}
}
// wait performs the Wait on the underlying command under lock, and only once
// (subsequent wait calls block until the Wait is finished). It's ok to call
// wait multiple times, and in parallel. The error from the initial Wait is
// cached and returned for all subsequent calls.
func (p *ParentHandle) wait() error {
p.waitLock.Lock()
defer p.waitLock.Unlock()
if p.waitDone {
return p.waitErr
}
p.waitErr = p.c.Wait()
p.waitDone = true
return p.waitErr
}
// Wait will wait for the child process to terminate of its own accord.
// It returns nil if the process exited cleanly with an exit status of 0,
// any other exit code or error will result in an appropriate error return
func (p *ParentHandle) Wait(timeout time.Duration) error {
c := make(chan error, 1)
go func() {
c <- p.wait()
close(c)
}()
// If timeout is zero time.After will panic; we handle zero specially
// to mean infinite timeout.
if timeout > 0 {
select {
case <-p.tk.After(timeout):
return verror.New(ErrTimeout, nil)
case err := <-c:
return err
}
} else {
return <-c
}
}
// Pid returns the pid of the child, 0 if the child process doesn't exist
func (p *ParentHandle) Pid() int {
if p.c.Process != nil {
return p.c.Process.Pid
}
return 0
}
// ChildPid returns the pid of a child process as reported by its status
// callback.
func (p *ParentHandle) ChildPid() int {
return p.callbackPid
}
// Exists returns true if the child process exists and can be signal'ed
func (p *ParentHandle) Exists() bool {
if p.c.Process != nil {
return syscall.Kill(p.c.Process.Pid, 0) == nil
}
return false
}
// Kill kills the child process.
func (p *ParentHandle) Kill() error {
if p.c.Process == nil {
return verror.New(errNoSuchProcess, nil)
}
return p.c.Process.Kill()
}
// Signal sends the given signal to the child process.
func (p *ParentHandle) Signal(sig syscall.Signal) error {
if p.c.Process == nil {
return verror.New(errNoSuchProcess, nil)
}
return syscall.Kill(p.c.Process.Pid, sig)
}
// Clean will clean up state, including killing the child process.
func (p *ParentHandle) Clean() error {
if err := p.Kill(); err != nil {
return err
}
return p.wait()
}
func encodeString(w io.Writer, data string) error {
l := len(data)
if err := binary.Write(w, binary.BigEndian, int64(l)); err != nil {
return err
}
if n, err := w.Write([]byte(data)); err != nil || n != l {
if err != nil {
return err
} else {
return verror.New(errPartialWrite, nil)
}
}
return nil
}