blob: 4df120b24074d805043b17e1828c57c84aa76027 [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 and 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 a shell in two ways: one for a subprocess and another
// for an inprocess function.
//
// - subprocesses are added using the AddSubprocess method in the parent
// and by the modules.RegisterChild function in the child process (typically
// RegisterChild is called from an init function). modules.Dispatch must
// be called in the child process to execute the subprocess 'Main' function
// provided to RegisterChild.
// - inprocess functions are added using the AddFunction method.
//
// 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.
//
// 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.
package modules
import (
"bufio"
"fmt"
"io"
"strings"
"sync"
"veyron2/vlog"
)
// Shell represents the context within which commands are run.
type Shell struct {
mu sync.Mutex
env map[string]string
cmds map[string]*commandDesc
handles map[Handle]struct{}
}
type commandDesc struct {
factory func() command
help string
}
type childRegistrar struct {
sync.Mutex
mains map[string]Main
}
var child = &childRegistrar{mains: make(map[string]Main)}
// NewShell creates a new instance of Shell.
func NewShell() *Shell {
return &Shell{
env: make(map[string]string),
cmds: make(map[string]*commandDesc),
handles: make(map[Handle]struct{}),
}
}
type Main func(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error
// AddSubprocess adds a new command to the Shell that will be run
// as a subprocess. In addition, the child process must call RegisterChild
// using the same name used here and provide the function to be executed
// in the child.
func (sh *Shell) AddSubprocess(name string, help string) {
if !child.hasCommand(name) {
vlog.Infof("Warning: %q is not registered with modules.Dispatcher", name)
}
entryPoint := shellEntryPoint + "=" + name
sh.mu.Lock()
sh.cmds[name] = &commandDesc{func() command { return newExecHandle(entryPoint) }, help}
sh.mu.Unlock()
}
// AddFunction adds a new command to the Shell that will be run
// within the current process.
func (sh *Shell) AddFunction(name string, main Main, help string) {
sh.mu.Lock()
sh.cmds[name] = &commandDesc{func() command { return newFunctionHandle(main) }, help}
sh.mu.Unlock()
}
// String returns a string representation of the Shell, which is a
// concatenation of the help messages of each Command currently available
// to it.
func (sh *Shell) String() string {
sh.mu.Lock()
defer sh.mu.Unlock()
h := ""
for n, _ := range sh.cmds {
h += n + ", "
}
return strings.TrimRight(h, ", ")
}
// Help returns the help message for the specified command.
func (sh *Shell) Help(command string) string {
sh.mu.Lock()
defer sh.mu.Unlock()
if c := sh.cmds[command]; c != nil {
return command + ": " + c.help
}
return ""
}
// Start starts the specified command, it returns a Handle which can be used
// for interacting with that command. The Shell tracks all of the Handles
// that it creates so that it can shut them down when asked to. If any
// application calls Shutdown on a handle directly, it must call the Forget
// method on the Shell instance hosting that Handle to avoid storage leaks.
func (sh *Shell) Start(command string, args ...string) (Handle, error) {
sh.mu.Lock()
cmd := sh.cmds[command]
if cmd == nil {
sh.mu.Unlock()
return nil, fmt.Errorf("command %q is not available", command)
}
expanded := sh.expand(args...)
sh.mu.Unlock()
h, err := cmd.factory().start(sh, expanded...)
if err != nil {
return nil, err
}
sh.mu.Lock()
sh.handles[h] = struct{}{}
sh.mu.Unlock()
return h, nil
}
// Forget tells the Shell to stop tracking the supplied Handle.
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.
func (sh *Shell) SetVar(key, value string) {
sh.mu.Lock()
defer sh.mu.Unlock()
// TODO(cnicolaou): expand value
sh.env[key] = value
}
// 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. Any buffered output from the command's stderr stream
// will be written to the supplied io.Writer. If the io.Writer is nil
// then any such output is lost.
func (sh *Shell) Cleanup(output io.Writer) {
sh.mu.Lock()
defer sh.mu.Unlock()
for k, _ := range sh.handles {
k.Shutdown(output)
}
sh.handles = make(map[Handle]struct{})
}
// Handle represents a running command.
type Handle interface {
// Stdout returns a buffered reader to the running command's stdout stream.
Stdout() *bufio.Reader
// Stderr returns an unbuffered 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.WriteCloser
// Shutdown closes the Stdin for the command. It is primarily intended
// for being called by the Shell, if other application code calls it
// then it should use the Shell's Forget method to have the Shell stop
// tracking the handle. Any buffered stderr output from the command will
// be written to the supplied io.Writer. If the io.Writer is nil then
// any such output is lost.
Shutdown(io.Writer)
}
// command is used to abstract the implementations of inprocess and subprocess
// commands.
type command interface {
start(sh *Shell, args ...string) (Handle, error)
}
func WaitForEOF(stdin io.Reader) {
buf := [1024]byte{}
for {
if _, err := stdin.Read(buf[:]); err == io.EOF {
return
}
}
}