blob: 535cc015090541a1abcd4f6856865f9043678364 [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 Simsaf57930f2014-11-05 15:19:31 -080052 "veyron.io/veyron/veyron/lib/exec"
Asim Shankar95910b62014-10-31 22:02:29 -070053 "veyron.io/veyron/veyron/lib/flags/consts"
Jiri Simsa519c5072014-09-17 21:37:57 -070054 "veyron.io/veyron/veyron2/vlog"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070055)
56
57// Shell represents the context within which commands are run.
58type Shell struct {
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -070059 mu sync.Mutex
60 env map[string]string
61 cmds map[string]*commandDesc
62 handles map[Handle]struct{}
63 credDir string
64 startTimeout time.Duration
Jiri Simsa37893392014-11-07 10:55:45 -080065 config exec.Config
Cosmos Nicolaou62613842014-08-25 21:57:37 -070066}
67
68type commandDesc struct {
69 factory func() command
70 help string
71}
72
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070073type childEntryPoint struct {
74 fn Main
75 help string
Cosmos Nicolaou62613842014-08-25 21:57:37 -070076}
77
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070078type childRegistrar struct {
79 sync.Mutex
80 mains map[string]*childEntryPoint
81}
82
83var child = &childRegistrar{mains: make(map[string]*childEntryPoint)}
Cosmos Nicolaou62613842014-08-25 21:57:37 -070084
Asim Shankar95910b62014-10-31 22:02:29 -070085// NewShell creates a new instance of Shell. If this new instance is is a test
86// and no credentials have been configured in the environment via
87// consts.VeyronCredentials then CreateAndUseNewCredentials will be used to
88// configure a new ID for the shell and its children. NewShell takes optional
89// regexp patterns that can be used to specify subprocess commands that are
90// implemented in the same binary as this shell (i.e. have been registered
91// using modules.RegisterChild) to be automatically added to it. If the
92// patterns fail to match any such command then they have no effect.
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -070093func NewShell(patterns ...string) *Shell {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070094 // TODO(cnicolaou): should create a new identity if one doesn't
95 // already exist
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -070096 sh := &Shell{
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -070097 env: make(map[string]string),
98 cmds: make(map[string]*commandDesc),
99 handles: make(map[Handle]struct{}),
100 startTimeout: time.Minute,
Jiri Simsa37893392014-11-07 10:55:45 -0800101 config: exec.NewConfig(),
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700102 }
Asim Shankar95910b62014-10-31 22:02:29 -0700103 if flag.Lookup("test.run") != nil && os.Getenv(consts.VeyronCredentials) == "" {
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700104 if err := sh.CreateAndUseNewCredentials(); err != nil {
105 // TODO(cnicolaou): return an error rather than panic.
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700106 panic(err)
107 }
108 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700109 for _, pattern := range patterns {
110 child.addSubprocesses(sh, pattern)
111 }
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700112 return sh
113}
114
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700115// CreateAndUseNewCredentials setups a new credentials directory and then
116// configures the shell and all of its children to use to it.
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700117//
118// TODO(cnicolaou): this should use the principal already setup
119// with the runtime if the runtime has been initialized, if not,
120// it should create a new principal. As of now, this approach only works
121// for child processes that talk to each other, but not to the parent
122// process that started them since it's running with a different set of
123// credentials setup elsewhere. When this change is made it should
124// be possible to remove creating credentials in many unit tests.
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700125func (sh *Shell) CreateAndUseNewCredentials() error {
126 dir, err := ioutil.TempDir("", "veyron_credentials")
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700127 if err != nil {
128 return err
129 }
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700130 sh.credDir = dir
Asim Shankar95910b62014-10-31 22:02:29 -0700131 sh.SetVar(consts.VeyronCredentials, sh.credDir)
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700132 return nil
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700133}
134
135type Main func(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error
136
137// AddSubprocess adds a new command to the Shell that will be run
138// as a subprocess. In addition, the child process must call RegisterChild
139// using the same name used here and provide the function to be executed
140// in the child.
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700141func (sh *Shell) AddSubprocess(name, help string) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700142 if !child.hasCommand(name) {
143 vlog.Infof("Warning: %q is not registered with modules.Dispatcher", name)
144 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700145 sh.addSubprocess(name, help)
146}
147
148func (sh *Shell) addSubprocess(name string, help string) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700149 sh.mu.Lock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700150 sh.cmds[name] = &commandDesc{func() command { return newExecHandle(name) }, help}
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700151 sh.mu.Unlock()
152}
153
154// AddFunction adds a new command to the Shell that will be run
155// within the current process.
156func (sh *Shell) AddFunction(name string, main Main, help string) {
157 sh.mu.Lock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700158 sh.cmds[name] = &commandDesc{func() command { return newFunctionHandle(name, main) }, help}
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700159 sh.mu.Unlock()
160}
161
162// String returns a string representation of the Shell, which is a
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700163// list of the commands currently available in the shell.
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700164func (sh *Shell) String() string {
165 sh.mu.Lock()
166 defer sh.mu.Unlock()
167 h := ""
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700168 for n, _ := range sh.cmds {
169 h += n + ", "
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700170 }
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700171 return strings.TrimRight(h, ", ")
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700172}
173
174// Help returns the help message for the specified command.
175func (sh *Shell) Help(command string) string {
176 sh.mu.Lock()
177 defer sh.mu.Unlock()
178 if c := sh.cmds[command]; c != nil {
Cosmos Nicolaou0f0e8772014-09-10 21:29:25 -0700179 return command + ": " + c.help
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700180 }
181 return ""
182}
183
184// Start starts the specified command, it returns a Handle which can be used
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700185// for interacting with that command. The OS and shell environment variables
186// are merged with the ones supplied as a parameter; the parameter specified
187// ones override the Shell and the Shell ones override the OS ones.
188//
189// The Shell tracks all of the Handles that it creates so that it can shut
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700190// them down when asked to. The returned Handle may be non-nil even when an
191// error is returned, in which case it may be used to retrieve any output
192// from the failed command.
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700193//
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700194// Commands may have already been registered with the Shell using AddFunction
195// or AddSubprocess, but if not, they will treated as subprocess commands
196// and an attempt made to run them. Such 'dynamically' started subprocess
197// commands are not remembered the by Shell and do not provide a 'help'
198// message etc; their handles are remembered and will be acted on by
199// the Cleanup method. If the non-registered subprocess command does not
200// exist then the Start command will return an error.
Cosmos Nicolaou612ad382014-10-29 19:41:35 -0700201func (sh *Shell) Start(name string, env []string, args ...string) (Handle, error) {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700202 cenv := sh.MergedEnv(env)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700203 cmd := sh.getCommand(name)
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700204 expanded := append([]string{name}, sh.expand(args...)...)
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700205 h, err := cmd.factory().start(sh, cenv, expanded...)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700206 if err != nil {
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700207 // If the error is a timeout, then h can be used to recover
208 // any output from the process.
209 return h, err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700210 }
211 sh.mu.Lock()
212 sh.handles[h] = struct{}{}
213 sh.mu.Unlock()
214 return h, nil
215}
216
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700217func (sh *Shell) getCommand(name string) *commandDesc {
218 sh.mu.Lock()
219 cmd := sh.cmds[name]
220 sh.mu.Unlock()
221 if cmd == nil {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700222 cmd = &commandDesc{func() command { return newExecHandle(name) }, ""}
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700223 }
224 return cmd
225}
226
Cosmos Nicolaoue5b41502014-10-29 22:55:09 -0700227// SetStartTimeout sets the timeout for starting subcommands.
228func (sh *Shell) SetStartTimeout(d time.Duration) {
229 sh.startTimeout = d
230}
231
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700232// CommandEnvelope returns the command line and environment that would be
233// used for running the subprocess or function if it were started with the
234// specifed arguments.
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700235func (sh *Shell) CommandEnvelope(name string, env []string, args ...string) ([]string, []string) {
236 return sh.getCommand(name).factory().envelope(sh, sh.MergedEnv(env), args...)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700237}
238
239// Forget tells the Shell to stop tracking the supplied Handle. This is
240// generally used when the application wants to control the order that
241// commands are shutdown in.
242func (sh *Shell) Forget(h Handle) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700243 sh.mu.Lock()
244 delete(sh.handles, h)
245 sh.mu.Unlock()
246}
247
248func (sh *Shell) expand(args ...string) []string {
249 exp := []string{}
250 for _, a := range args {
251 if len(a) > 0 && a[0] == '$' {
252 if v, present := sh.env[a[1:]]; present {
253 exp = append(exp, v)
254 continue
255 }
256 }
257 exp = append(exp, a)
258 }
259 return exp
260}
261
262// GetVar returns the variable associated with the specified key
263// and an indication of whether it is defined or not.
264func (sh *Shell) GetVar(key string) (string, bool) {
265 sh.mu.Lock()
266 defer sh.mu.Unlock()
267 v, present := sh.env[key]
268 return v, present
269}
270
271// SetVar sets the value to be associated with key.
272func (sh *Shell) SetVar(key, value string) {
273 sh.mu.Lock()
274 defer sh.mu.Unlock()
275 // TODO(cnicolaou): expand value
276 sh.env[key] = value
277}
278
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700279// ClearVar removes the speficied variable from the Shell's environment
280func (sh *Shell) ClearVar(key string) {
281 sh.mu.Lock()
282 defer sh.mu.Unlock()
283 delete(sh.env, key)
284}
285
Jiri Simsa37893392014-11-07 10:55:45 -0800286// GetConfigKey returns the value associated with the specified key in
287// the Shell's config and an indication of whether it is defined or
288// not.
289func (sh *Shell) GetConfigKey(key string) (string, bool) {
290 v, err := sh.config.Get(key)
291 return v, err == nil
292}
293
294// SetConfigKey sets the value of the specified key in the Shell's
295// config.
296func (sh *Shell) SetConfigKey(key, value string) {
297 sh.config.Set(key, value)
298}
299
300// ClearConfigKey removes the speficied key from the Shell's config.
301func (sh *Shell) ClearConfigKey(key string) {
302 sh.config.Clear(key)
303}
304
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700305// Env returns the entire set of environment variables associated with this
306// Shell as a string slice.
307func (sh *Shell) Env() []string {
308 vars := []string{}
309 sh.mu.Lock()
310 defer sh.mu.Unlock()
311 for k, v := range sh.env {
312 vars = append(vars, k+"="+v)
313 }
314 return vars
315}
316
317// Cleanup calls Shutdown on all of the Handles currently being tracked
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700318// by the Shell and writes to stdout and stderr as per the Shutdown
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700319// method in the Handle interface. Cleanup returns the error from the
320// last Shutdown that returned a non-nil error. The order that the
321// Shutdown routines are executed is not defined.
322func (sh *Shell) Cleanup(stdout, stderr io.Writer) error {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700323 sh.mu.Lock()
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700324 handles := make(map[Handle]struct{})
325 for k, v := range sh.handles {
326 handles[k] = v
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700327 }
328 sh.handles = make(map[Handle]struct{})
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700329 sh.mu.Unlock()
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700330 var err error
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700331 for k, _ := range handles {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700332 cerr := k.Shutdown(stdout, stderr)
333 if cerr != nil {
334 err = cerr
335 }
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700336 }
Cosmos Nicolaouf47699b2014-10-10 14:36:23 -0700337 if len(sh.credDir) > 0 {
338 os.RemoveAll(sh.credDir)
Cosmos Nicolaou9afe1202014-09-19 13:45:57 -0700339 }
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700340 return err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700341}
342
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700343// MergedEnv returns a slice that contains the merged set of environment
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700344// variables from the OS environment, those in this Shell and those provided
345// as a parameter to it. It prefers values from its parameter over those
346// from the Shell, over those from the OS.
347func (sh *Shell) MergedEnv(env []string) []string {
348 osmap := envSliceToMap(os.Environ())
349 evmap := envSliceToMap(env)
350 sh.mu.Lock()
351 m1 := mergeMaps(osmap, sh.env)
352 defer sh.mu.Unlock()
353 m2 := mergeMaps(m1, evmap)
354 r := []string{}
355 for k, v := range m2 {
356 r = append(r, k+"="+v)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700357 }
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700358 return r
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700359}
360
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700361// Handle represents a running command.
362type Handle interface {
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700363 // Stdout returns a reader to the running command's stdout stream.
364 Stdout() io.Reader
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700365
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700366 // Stderr returns a reader to the running command's stderr
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700367 // stream.
368 Stderr() io.Reader
369
370 // Stdin returns a writer to the running command's stdin. The
371 // convention is for commands to wait for stdin to be closed before
372 // they exit, thus the caller should close stdin when it wants the
373 // command to exit cleanly.
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700374 Stdin() io.Writer
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700375
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700376 // CloseStdin closes stdin in a manner that avoids a data race
377 // between any current readers on it.
378 CloseStdin()
379
380 // Shutdown closes the Stdin for the command and then reads output
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700381 // from the command's stdout until it encounters EOF, waits for
382 // the command to complete and then reads all of its stderr output.
383 // The stdout and stderr contents are written to the corresponding
384 // io.Writers if they are non-nil, otherwise the content is discarded.
385 Shutdown(stdout, stderr io.Writer) error
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700386
387 // Pid returns the pid of the process running the command
388 Pid() int
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700389}
390
391// command is used to abstract the implementations of inprocess and subprocess
392// commands.
393type command interface {
Cosmos Nicolaou2cb05fb2014-10-23 13:37:32 -0700394 envelope(sh *Shell, env []string, args ...string) ([]string, []string)
395 start(sh *Shell, env []string, args ...string) (Handle, error)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700396}