blob: 687ccb8bdc22db7ef3aa4fb4e80020e83d0620d0 [file] [log] [blame]
// Package modules provides a mechanism for running commonly used services
// as subprocesses and client functionality for accessing those services.
// Such services and functions are collectively called 'commands' and are
// registered with a single, per-process, registry, but executed within a
// context, defined by the Shell type. The Shell is analagous to the original
// UNIX shell and maintains a key, value store of variables that is accessible
// to all of the commands that it hosts. These variables may be referenced by
// the arguments passed to commands.
//
// Commands are added to the registry in two ways:
// - via RegisterChild for a subprocess
// - via RegisterFunction for an in-process function
//
// In all cases commands are started by invoking the Start method on the
// Shell with the name of the command to run. An instance of the Handle
// interface is returned which can be used to interact with the function
// or subprocess, and in particular to read/write data from/to it using io
// channels that follow the stdin, stdout, stderr convention.
//
// A simple protocol must be followed by all commands, namely, they
// should wait for their stdin stream to be closed before exiting. The
// caller can then coordinate with any command by writing to that stdin
// stream and reading responses from the stdout stream, and it can close
// stdin when it's ready for the command to exit using the CloseStdin method
// on the command's handle.
//
// The signature of the function that implements the command is the
// same for both types of command and is defined by the Main function type.
// In particular stdin, stdout and stderr are provided as parameters, as is
// a map representation of the shell's environment.
//
// By default, every Shell created by NewShell starts a security agent
// to manage principals for child processes. These default
// credentials can be overridden by passing a nil context to NewShell
// then specifying VeyronCredentials in the environment provided as a
// parameter to the Start method.
package modules
import (
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"os"
"sync"
"syscall"
"time"
"v.io/core/veyron2/security"
"v.io/core/veyron/lib/exec"
"v.io/core/veyron/lib/flags/consts"
"v.io/core/veyron/security/agent"
"v.io/core/veyron/security/agent/keymgr"
"v.io/core/veyron2"
"v.io/core/veyron2/context"
)
const (
shellBlessingExtension = "test-shell"
childBlessingExtension = "child"
)
// Shell represents the context within which commands are run.
type Shell struct {
mu sync.Mutex
env map[string]string
handles map[Handle]struct{}
// tmpCredDir is the temporary directory created by this
// shell. This must be removed when the shell is cleaned up.
tempCredDir string
startTimeout, waitTimeout time.Duration
config exec.Config
principal security.Principal
blessing security.Blessings
agent *keymgr.Agent
ctx *context.T
cancelCtx func()
}
// NewShell creates a new instance of Shell.
// If ctx is non-nil, the shell will manage Principals for child processes.
// By default it adds a blessing to ctx's principal to ensure isolation. Any child processes
// are created with principals derived from this new blessing.
// However, if p is non-nil the shell will skip the new blessing and child processes
// will have their principals derived from p's default blessing(s).
func NewShell(ctx *context.T, p security.Principal) (*Shell, error) {
sh := &Shell{
env: make(map[string]string),
handles: make(map[Handle]struct{}),
startTimeout: time.Minute,
waitTimeout: 10 * time.Second,
config: exec.NewConfig(),
}
if ctx == nil {
return sh, nil
}
var err error
ctx, sh.cancelCtx = context.WithCancel(ctx)
if ctx, _, err = veyron2.SetNewStreamManager(ctx); err != nil {
return nil, err
}
sh.ctx = ctx
if sh.tempCredDir, err = ioutil.TempDir("", "shell_credentials"); err != nil {
return nil, err
}
if sh.agent, err = keymgr.NewLocalAgent(ctx, sh.tempCredDir, nil); err != nil {
return nil, err
}
if p != nil {
sh.principal = p
sh.blessing = p.BlessingStore().Default()
return sh, nil
}
p = veyron2.GetPrincipal(ctx)
sh.principal = p
gen := rand.New(rand.NewSource(time.Now().UnixNano()))
// Use a unique blessing tree per shell.
blessingName := fmt.Sprintf("%s-%d", shellBlessingExtension, gen.Int63())
blessing, err := p.BlessSelf(blessingName)
if err != nil {
return nil, err
}
sh.blessing = blessing
if _, err := p.BlessingStore().Set(blessing, security.BlessingPattern(blessingName+security.ChainSeparator+childBlessingExtension)); err != nil {
return nil, err
}
if err := p.AddToRoots(blessing); err != nil {
return nil, err
}
// Our blessing store now contains a blessing with our unique prefix
// and the principal has that blessing's root added to its trusted
// list so that it will accept blessings derived from it.
return sh, nil
}
func (sh *Shell) getChildCredentials() (c *os.File, err error) {
if sh.ctx == nil {
return nil, nil
}
root := sh.principal
rootBlessing := sh.blessing
_, conn, err := sh.agent.NewPrincipal(sh.ctx, true)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
conn.Close()
}
}()
ctx, cancel := context.WithCancel(sh.ctx)
defer cancel()
if ctx, _, err = veyron2.SetNewStreamManager(ctx); err != nil {
return nil, err
}
syscall.ForkLock.RLock()
fd, err := syscall.Dup(int(conn.Fd()))
if err != nil {
syscall.ForkLock.RUnlock()
return nil, err
}
syscall.CloseOnExec(fd)
syscall.ForkLock.RUnlock()
p, err := agent.NewAgentPrincipal(ctx, fd, veyron2.GetClient(ctx))
if err != nil {
syscall.Close(fd)
return nil, err
}
blessingForChild, err := root.Bless(p.PublicKey(), rootBlessing, childBlessingExtension, security.UnconstrainedUse())
if err != nil {
return nil, err
}
if err := p.BlessingStore().SetDefault(blessingForChild); err != nil {
return nil, err
}
if _, err := p.BlessingStore().Set(blessingForChild, security.AllPrincipals); err != nil {
return nil, err
}
if err := p.AddToRoots(blessingForChild); err != nil {
return nil, err
}
if sh.blessing != nil {
// Create a second blessing for the child, that will be accepted
// by the parent, should the child choose to invoke RPCs on the parent.
blessingFromChild, err := root.Bless(p.PublicKey(), root.BlessingStore().Default(), childBlessingExtension, security.UnconstrainedUse())
if err != nil {
return nil, err
}
// We store this blessing as the one to use with the patterns that match
// the root's names.
for rootName, _ := range root.BlessingsInfo(root.BlessingStore().Default()) {
if _, err := p.BlessingStore().Set(blessingFromChild, security.BlessingPattern(rootName)); err != nil {
return nil, err
}
}
if err := p.AddToRoots(blessingFromChild); err != nil {
return nil, err
}
}
return conn, nil
}
type Main func(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error
// String returns a string representation of the Shell, which is a
// list of the commands currently available in the shell.
func (sh *Shell) String() string {
return registry.help("")
}
// Help returns the help message for the specified command.
func (sh *Shell) Help(command string) string {
return registry.help(command)
}
func (sh *Shell) StartExternalCommand(env []string, args ...string) (Handle, error) {
if len(args) == 0 {
return nil, errors.New("no arguments specified to StartExternalCommand")
}
c := newExecHandleForExternalCommand(args[0])
return sh.startCommand(c, env, args...)
}
// Start starts the specified command, it returns a Handle which can be
// used for interacting with that command.
//
// The environment variables for the command are set by merging variables
// from the OS environment, those in this Shell and those provided as a
// parameter to it. In general, it prefers values from its parameter over
// those from the Shell, over those from the OS. However, the VeyronCredentials
// environment variable is handled specially.
//
// If the VeyronCredentials environment variable is set in 'env' then that
// is the value that gets used. If the shell's VeyronCredentials are set then
// VeyronCredentials for the command are set to a freshly created directory
// specifying a principal blessed by the shell's credentials. In all other
// cases VeyronCredentials for the command remains unset.
//
// The Shell tracks all of the Handles that it creates so that it can shut
// them down when asked to. The returned Handle may be non-nil even when an
// error is returned, in which case it may be used to retrieve any output
// from the failed command.
//
// Commands must have already been registered using RegisterFunction
// or RegisterChild.
func (sh *Shell) Start(name string, env []string, args ...string) (Handle, error) {
cmd := registry.getCommand(name)
if cmd == nil {
return nil, fmt.Errorf("%s: not registered", name)
}
expanded := append([]string{name}, sh.expand(args...)...)
c := cmd.factory()
h, err := sh.startCommand(c, env, expanded...)
if err != nil {
// If the error is a timeout, then h can be used to recover
// any output from the process.
return h, err
}
if err := h.WaitForReady(sh.waitTimeout); err != nil {
return h, err
}
return h, nil
}
func (sh *Shell) startCommand(c command, env []string, args ...string) (Handle, error) {
cenv, err := sh.setupCommandEnv(env)
if err != nil {
return nil, err
}
p, err := sh.getChildCredentials()
if err != nil {
return nil, err
}
h, err := c.start(sh, p, cenv, args...)
if err != nil {
return nil, err
}
sh.mu.Lock()
sh.handles[h] = struct{}{}
sh.mu.Unlock()
return h, nil
}
// SetStartTimeout sets the timeout for starting subcommands.
func (sh *Shell) SetStartTimeout(d time.Duration) {
sh.startTimeout = d
}
// SetWaitTimeout sets the timeout for waiting on subcommands to complete.
func (sh *Shell) SetWaitTimeout(d time.Duration) {
sh.waitTimeout = d
}
// CommandEnvelope returns the command line and environment that would be
// used for running the subprocess or function if it were started with the
// specifed arguments.
//
// This method is not idempotent as the directory pointed to by the
// VeyronCredentials environment variable may be freshly created with
// each invocation.
func (sh *Shell) CommandEnvelope(name string, env []string, args ...string) ([]string, []string) {
cmd := registry.getCommand(name)
if cmd == nil {
return []string{}, []string{}
}
menv, err := sh.setupCommandEnv(env)
if err != nil {
return []string{}, []string{}
}
return cmd.factory().envelope(sh, menv, args...)
}
// Forget tells the Shell to stop tracking the supplied Handle. This is
// generally used when the application wants to control the order that
// commands are shutdown in.
func (sh *Shell) Forget(h Handle) {
sh.mu.Lock()
delete(sh.handles, h)
sh.mu.Unlock()
}
func (sh *Shell) expand(args ...string) []string {
exp := []string{}
for _, a := range args {
if len(a) > 0 && a[0] == '$' {
if v, present := sh.env[a[1:]]; present {
exp = append(exp, v)
continue
}
}
exp = append(exp, a)
}
return exp
}
// GetVar returns the variable associated with the specified key
// and an indication of whether it is defined or not.
func (sh *Shell) GetVar(key string) (string, bool) {
sh.mu.Lock()
defer sh.mu.Unlock()
v, present := sh.env[key]
return v, present
}
// SetVar sets the value to be associated with key.
//
// Note that setting the VeyronCredentials environement
// variable changes the shell's principal which is used
// for blessing the principals supplied to the shell's
// children.
func (sh *Shell) SetVar(key, value string) {
sh.mu.Lock()
defer sh.mu.Unlock()
// TODO(cnicolaou): expand value
sh.env[key] = value
}
// ClearVar removes the speficied variable from the Shell's environment
//
// Note that clearing the VeyronCredentials environment variable
// would amount to clearing the shell's principal, and therefore, any
// children of this shell would have their VeyronCredentials environment
// variable set only if it is explicitly set in their parameters.
func (sh *Shell) ClearVar(key string) {
sh.mu.Lock()
defer sh.mu.Unlock()
delete(sh.env, key)
}
// GetConfigKey returns the value associated with the specified key in
// the Shell's config and an indication of whether it is defined or
// not.
func (sh *Shell) GetConfigKey(key string) (string, bool) {
v, err := sh.config.Get(key)
return v, err == nil
}
// SetConfigKey sets the value of the specified key in the Shell's
// config.
func (sh *Shell) SetConfigKey(key, value string) {
sh.config.Set(key, value)
}
// ClearConfigKey removes the speficied key from the Shell's config.
func (sh *Shell) ClearConfigKey(key string) {
sh.config.Clear(key)
}
// Env returns the entire set of environment variables associated with this
// Shell as a string slice.
func (sh *Shell) Env() []string {
vars := []string{}
sh.mu.Lock()
defer sh.mu.Unlock()
for k, v := range sh.env {
vars = append(vars, k+"="+v)
}
return vars
}
// Cleanup calls Shutdown on all of the Handles currently being tracked
// by the Shell and writes to stdout and stderr as per the Shutdown
// method in the Handle interface. Cleanup returns the error from the
// last Shutdown that returned a non-nil error. The order that the
// Shutdown routines are executed is not defined.
func (sh *Shell) Cleanup(stdout, stderr io.Writer) error {
sh.mu.Lock()
handles := make(map[Handle]struct{})
for k, v := range sh.handles {
handles[k] = v
}
sh.handles = make(map[Handle]struct{})
sh.mu.Unlock()
var err error
for k, _ := range handles {
cerr := k.Shutdown(stdout, stderr)
if cerr != nil {
err = cerr
}
}
if sh.cancelCtx != nil {
// Note(ribrdb, caprita): This will shutdown the agents. If there
// were errors shutting down it is possible there could be child
// processes still running, and stopping the agent may cause
// additional failures.
sh.cancelCtx()
}
os.RemoveAll(sh.tempCredDir)
return err
}
func (sh *Shell) setupCommandEnv(env []string) ([]string, error) {
osmap := envSliceToMap(os.Environ())
evmap := envSliceToMap(env)
sh.mu.Lock()
defer sh.mu.Unlock()
m1 := mergeMaps(osmap, sh.env)
// Clear any VeyronCredentials directory in m1 as we never
// want the child to directly use the directory specified
// by the shell's VeyronCredentials.
delete(m1, consts.VeyronCredentials)
delete(m1, agent.FdVarName)
m2 := mergeMaps(m1, evmap)
r := []string{}
for k, v := range m2 {
r = append(r, k+"="+v)
}
return r, nil
}
// Handle represents a running command.
type Handle interface {
// Stdout returns a reader to the running command's stdout stream.
Stdout() io.Reader
// Stderr returns a reader to the running command's stderr
// stream.
Stderr() io.Reader
// Stdin returns a writer to the running command's stdin. The
// convention is for commands to wait for stdin to be closed before
// they exit, thus the caller should close stdin when it wants the
// command to exit cleanly.
Stdin() io.Writer
// CloseStdin closes stdin in a manner that avoids a data race
// between any current readers on it.
CloseStdin()
// Shutdown closes the Stdin for the command and then reads output
// from the command's stdout until it encounters EOF, waits for
// the command to complete and then reads all of its stderr output.
// The stdout and stderr contents are written to the corresponding
// io.Writers if they are non-nil, otherwise the content is discarded.
Shutdown(stdout, stderr io.Writer) error
// Pid returns the pid of the process running the command
Pid() int
// WaitForReady waits until the child process signals to us that it is
// ready. If this does not occur within the given timeout duration, a
// timeout error is returned.
WaitForReady(timeout time.Duration) error
}
// command is used to abstract the implementations of inprocess and subprocess
// commands.
type command interface {
envelope(sh *Shell, env []string, args ...string) ([]string, []string)
start(sh *Shell, agent *os.File, env []string, args ...string) (Handle, error)
}