blob: 1bf3e61d46a195042569211bd02de2a77d1d3d29 [file] [log] [blame]
Cosmos Nicolaou62613842014-08-25 21:57:37 -07001// Package modules provides a mechanism for running commonly used services
2// as subprocesses and client functionality for accessing those services.
3// Such services and functions are collectively called 'commands' and are
4// registered with and executed within a context, defined by the Shell type.
5// The Shell is analagous to the original UNIX shell and maintains a
6// key, value store of variables that is accessible to all of the commands that
7// it hosts. These variables may be referenced by the arguments passed to
8// commands.
9//
10// Commands are added to a shell in two ways: one for a subprocess and another
11// for an inprocess function.
12//
13// - subprocesses are added using the AddSubprocess method in the parent
14// and by the modules.RegisterChild function in the child process (typically
15// RegisterChild is called from an init function). modules.Dispatch must
16// be called in the child process to execute the subprocess 'Main' function
17// provided to RegisterChild.
18// - inprocess functions are added using the AddFunction method.
19//
20// In all cases commands are started by invoking the Start method on the
21// Shell with the name of the command to run. An instance of the Handle
22// interface is returned which can be used to interact with the function
23// or subprocess, and in particular to read/write data from/to it using io
24// channels that follow the stdin, stdout, stderr convention.
25//
26// A simple protocol must be followed by all commands, namely, they
27// should wait for their stdin stream to be closed before exiting. The
28// caller can then coordinate with any command by writing to that stdin
29// stream and reading responses from the stdout stream, and it can close
Cosmos Nicolaou66afced2014-09-15 22:12:43 -070030// stdin when it's ready for the command to exit using the CloseStdin method
31// on the command's handle.
Cosmos Nicolaou62613842014-08-25 21:57:37 -070032//
33// The signature of the function that implements the command is the
34// same for both types of command and is defined by the Main function type.
35// In particular stdin, stdout and stderr are provided as parameters, as is
36// a map representation of the shell's environment.
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070037//
38// If a Shell is created within a unit test then it will automatically
39// generate a security ID, write it to a file and set the appropriate
40// environment variable to refer to it.
Cosmos Nicolaou62613842014-08-25 21:57:37 -070041package modules
42
43import (
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070044 "flag"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070045 "io"
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070046 "io/ioutil"
47 "os"
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -070048 "strings"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070049 "sync"
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -070050 "time"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070051
Jiri Simsa519c5072014-09-17 21:37:57 -070052 "veyron.io/veyron/veyron2/vlog"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070053)
54
55// Shell represents the context within which commands are run.
56type Shell struct {
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -070057 mu sync.Mutex
58 env map[string]string
59 cmds map[string]*commandDesc
60 handles map[Handle]struct{}
61 credDir string
62 startTimeout time.Duration
Cosmos Nicolaou62613842014-08-25 21:57:37 -070063}
64
65type commandDesc struct {
66 factory func() command
67 help string
68}
69
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070070type childEntryPoint struct {
71 fn Main
72 help string
Cosmos Nicolaou62613842014-08-25 21:57:37 -070073}
74
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070075type childRegistrar struct {
76 sync.Mutex
77 mains map[string]*childEntryPoint
78}
79
80var child = &childRegistrar{mains: make(map[string]*childEntryPoint)}
Cosmos Nicolaou62613842014-08-25 21:57:37 -070081
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070082// NewShell creates a new instance of Shell. If this new instance is
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -070083// is a test and no credentials have been configured in the environment
84// via VEYRON_CREDENTIALS then CreateAndUseNewCredentials will be used to
85// configure a new ID for the shell and its children.
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070086// NewShell takes optional regexp patterns that can be used to specify
87// subprocess commands that are implemented in the same binary as this shell
88// (i.e. have been registered using modules.RegisterChild) to be
89// automatically added to it. If the patterns fail to match any such command
90// then they have no effect.
91func NewShell(patterns ...string) *Shell {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070092 // TODO(cnicolaou): should create a new identity if one doesn't
93 // already exist
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070094 sh := &Shell{
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -070095 env: make(map[string]string),
96 cmds: make(map[string]*commandDesc),
97 handles: make(map[Handle]struct{}),
98 startTimeout: time.Minute,
Cosmos Nicolaou62613842014-08-25 21:57:37 -070099 }
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700100 if flag.Lookup("test.run") != nil && os.Getenv("VEYRON_CREDENTIALS") == "" {
101 if err := sh.CreateAndUseNewCredentials(); err != nil {
102 // TODO(cnicolaou): return an error rather than panic.
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700103 panic(err)
104 }
105 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700106 for _, pattern := range patterns {
107 child.addSubprocesses(sh, pattern)
108 }
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700109 return sh
110}
111
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700112// CreateAndUseNewCredentials setups a new credentials directory and then
113// configures the shell and all of its children to use to it.
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700114//
115// TODO(cnicolaou): this should use the principal already setup
116// with the runtime if the runtime has been initialized, if not,
117// it should create a new principal. As of now, this approach only works
118// for child processes that talk to each other, but not to the parent
119// process that started them since it's running with a different set of
120// credentials setup elsewhere. When this change is made it should
121// be possible to remove creating credentials in many unit tests.
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700122func (sh *Shell) CreateAndUseNewCredentials() error {
123 dir, err := ioutil.TempDir("", "veyron_credentials")
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700124 if err != nil {
125 return err
126 }
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700127 sh.credDir = dir
128 sh.SetVar("VEYRON_CREDENTIALS", sh.credDir)
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700129 return nil
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700130}
131
132type Main func(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error
133
134// AddSubprocess adds a new command to the Shell that will be run
135// as a subprocess. In addition, the child process must call RegisterChild
136// using the same name used here and provide the function to be executed
137// in the child.
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700138func (sh *Shell) AddSubprocess(name, help string) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700139 if !child.hasCommand(name) {
140 vlog.Infof("Warning: %q is not registered with modules.Dispatcher", name)
141 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700142 sh.addSubprocess(name, help)
143}
144
145func (sh *Shell) addSubprocess(name string, help string) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700146 sh.mu.Lock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700147 sh.cmds[name] = &commandDesc{func() command { return newExecHandle(name) }, help}
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700148 sh.mu.Unlock()
149}
150
151// AddFunction adds a new command to the Shell that will be run
152// within the current process.
153func (sh *Shell) AddFunction(name string, main Main, help string) {
154 sh.mu.Lock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700155 sh.cmds[name] = &commandDesc{func() command { return newFunctionHandle(name, main) }, help}
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700156 sh.mu.Unlock()
157}
158
159// String returns a string representation of the Shell, which is a
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700160// list of the commands currently available in the shell.
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700161func (sh *Shell) String() string {
162 sh.mu.Lock()
163 defer sh.mu.Unlock()
164 h := ""
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700165 for n, _ := range sh.cmds {
166 h += n + ", "
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700167 }
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700168 return strings.TrimRight(h, ", ")
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700169}
170
171// Help returns the help message for the specified command.
172func (sh *Shell) Help(command string) string {
173 sh.mu.Lock()
174 defer sh.mu.Unlock()
175 if c := sh.cmds[command]; c != nil {
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700176 return command + ": " + c.help
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700177 }
178 return ""
179}
180
181// Start starts the specified command, it returns a Handle which can be used
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700182// for interacting with that command. The OS and shell environment variables
183// are merged with the ones supplied as a parameter; the parameter specified
184// ones override the Shell and the Shell ones override the OS ones.
185//
186// The Shell tracks all of the Handles that it creates so that it can shut
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700187// them down when asked to. The returned Handle may be non-nil even when an
188// error is returned, in which case it may be used to retrieve any output
189// from the failed command.
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700190//
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700191// Commands may have already been registered with the Shell using AddFunction
192// or AddSubprocess, but if not, they will treated as subprocess commands
193// and an attempt made to run them. Such 'dynamically' started subprocess
194// commands are not remembered the by Shell and do not provide a 'help'
195// message etc; their handles are remembered and will be acted on by
196// the Cleanup method. If the non-registered subprocess command does not
197// exist then the Start command will return an error.
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700198func (sh *Shell) Start(name string, env []string, args ...string) (Handle, error) {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700199 cenv := sh.MergedEnv(env)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700200 cmd := sh.getCommand(name)
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700201 expanded := append([]string{name}, sh.expand(args...)...)
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700202 h, err := cmd.factory().start(sh, cenv, expanded...)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700203 if err != nil {
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700204 // If the error is a timeout, then h can be used to recover
205 // any output from the process.
206 return h, err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700207 }
208 sh.mu.Lock()
209 sh.handles[h] = struct{}{}
210 sh.mu.Unlock()
211 return h, nil
212}
213
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700214func (sh *Shell) getCommand(name string) *commandDesc {
215 sh.mu.Lock()
216 cmd := sh.cmds[name]
217 sh.mu.Unlock()
218 if cmd == nil {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700219 cmd = &commandDesc{func() command { return newExecHandle(name) }, ""}
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700220 }
221 return cmd
222}
223
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700224// SetStartTimeout sets the timeout for starting subcommands.
225func (sh *Shell) SetStartTimeout(d time.Duration) {
226 sh.startTimeout = d
227}
228
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700229// CommandEnvelope returns the command line and environment that would be
230// used for running the subprocess or function if it were started with the
231// specifed arguments.
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700232func (sh *Shell) CommandEnvelope(name string, env []string, args ...string) ([]string, []string) {
233 return sh.getCommand(name).factory().envelope(sh, sh.MergedEnv(env), args...)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700234}
235
236// Forget tells the Shell to stop tracking the supplied Handle. This is
237// generally used when the application wants to control the order that
238// commands are shutdown in.
239func (sh *Shell) Forget(h Handle) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700240 sh.mu.Lock()
241 delete(sh.handles, h)
242 sh.mu.Unlock()
243}
244
245func (sh *Shell) expand(args ...string) []string {
246 exp := []string{}
247 for _, a := range args {
248 if len(a) > 0 && a[0] == '$' {
249 if v, present := sh.env[a[1:]]; present {
250 exp = append(exp, v)
251 continue
252 }
253 }
254 exp = append(exp, a)
255 }
256 return exp
257}
258
259// GetVar returns the variable associated with the specified key
260// and an indication of whether it is defined or not.
261func (sh *Shell) GetVar(key string) (string, bool) {
262 sh.mu.Lock()
263 defer sh.mu.Unlock()
264 v, present := sh.env[key]
265 return v, present
266}
267
268// SetVar sets the value to be associated with key.
269func (sh *Shell) SetVar(key, value string) {
270 sh.mu.Lock()
271 defer sh.mu.Unlock()
272 // TODO(cnicolaou): expand value
273 sh.env[key] = value
274}
275
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700276// ClearVar removes the speficied variable from the Shell's environment
277func (sh *Shell) ClearVar(key string) {
278 sh.mu.Lock()
279 defer sh.mu.Unlock()
280 delete(sh.env, key)
281}
282
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700283// Env returns the entire set of environment variables associated with this
284// Shell as a string slice.
285func (sh *Shell) Env() []string {
286 vars := []string{}
287 sh.mu.Lock()
288 defer sh.mu.Unlock()
289 for k, v := range sh.env {
290 vars = append(vars, k+"="+v)
291 }
292 return vars
293}
294
295// Cleanup calls Shutdown on all of the Handles currently being tracked
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700296// by the Shell and writes to stdout and stderr as per the Shutdown
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700297// method in the Handle interface. Cleanup returns the error from the
298// last Shutdown that returned a non-nil error. The order that the
299// Shutdown routines are executed is not defined.
300func (sh *Shell) Cleanup(stdout, stderr io.Writer) error {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700301 sh.mu.Lock()
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700302 handles := make(map[Handle]struct{})
303 for k, v := range sh.handles {
304 handles[k] = v
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700305 }
306 sh.handles = make(map[Handle]struct{})
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700307 sh.mu.Unlock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700308 var err error
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700309 for k, _ := range handles {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700310 cerr := k.Shutdown(stdout, stderr)
311 if cerr != nil {
312 err = cerr
313 }
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700314 }
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700315 if len(sh.credDir) > 0 {
316 os.RemoveAll(sh.credDir)
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700317 }
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700318 return err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700319}
320
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700321// MergedEnv returns a slice that contains the merged set of environment
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700322// variables from the OS environment, those in this Shell and those provided
323// as a parameter to it. It prefers values from its parameter over those
324// from the Shell, over those from the OS.
325func (sh *Shell) MergedEnv(env []string) []string {
326 osmap := envSliceToMap(os.Environ())
327 evmap := envSliceToMap(env)
328 sh.mu.Lock()
329 m1 := mergeMaps(osmap, sh.env)
330 defer sh.mu.Unlock()
331 m2 := mergeMaps(m1, evmap)
332 r := []string{}
333 for k, v := range m2 {
334 r = append(r, k+"="+v)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700335 }
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700336 return r
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700337}
338
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700339// Handle represents a running command.
340type Handle interface {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700341 // Stdout returns a reader to the running command's stdout stream.
342 Stdout() io.Reader
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700343
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700344 // Stderr returns a reader to the running command's stderr
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700345 // stream.
346 Stderr() io.Reader
347
348 // Stdin returns a writer to the running command's stdin. The
349 // convention is for commands to wait for stdin to be closed before
350 // they exit, thus the caller should close stdin when it wants the
351 // command to exit cleanly.
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700352 Stdin() io.Writer
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700353
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700354 // CloseStdin closes stdin in a manner that avoids a data race
355 // between any current readers on it.
356 CloseStdin()
357
358 // Shutdown closes the Stdin for the command and then reads output
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700359 // from the command's stdout until it encounters EOF, waits for
360 // the command to complete and then reads all of its stderr output.
361 // The stdout and stderr contents are written to the corresponding
362 // io.Writers if they are non-nil, otherwise the content is discarded.
363 Shutdown(stdout, stderr io.Writer) error
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700364
365 // Pid returns the pid of the process running the command
366 Pid() int
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700367}
368
369// command is used to abstract the implementations of inprocess and subprocess
370// commands.
371type command interface {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700372 envelope(sh *Shell, env []string, args ...string) ([]string, []string)
373 start(sh *Shell, env []string, args ...string) (Handle, error)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700374}