Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 1 | package modules |
| 2 | |
| 3 | import ( |
| 4 | "flag" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "os" |
| 8 | "strings" |
| 9 | "sync" |
| 10 | "time" |
| 11 | |
Jiri Simsa | 764efb7 | 2014-12-25 20:57:03 -0800 | [diff] [blame] | 12 | "v.io/core/veyron2/vlog" |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 13 | |
Jiri Simsa | 764efb7 | 2014-12-25 20:57:03 -0800 | [diff] [blame] | 14 | vexec "v.io/core/veyron/lib/exec" |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 15 | ) |
| 16 | |
| 17 | type commandDesc struct { |
| 18 | factory func() command |
| 19 | main Main |
| 20 | help string |
| 21 | } |
| 22 | |
| 23 | type cmdRegistry struct { |
| 24 | sync.Mutex |
| 25 | cmds map[string]*commandDesc |
| 26 | } |
| 27 | |
| 28 | var registry = &cmdRegistry{cmds: make(map[string]*commandDesc)} |
| 29 | |
| 30 | func (r *cmdRegistry) addCommand(name, help string, factory func() command, main Main) { |
| 31 | r.Lock() |
| 32 | defer r.Unlock() |
| 33 | r.cmds[name] = &commandDesc{factory, main, help} |
| 34 | } |
| 35 | |
| 36 | func (r *cmdRegistry) getCommand(name string) *commandDesc { |
| 37 | r.Lock() |
| 38 | defer r.Unlock() |
| 39 | return r.cmds[name] |
| 40 | } |
| 41 | |
James Ring | 9d9489d | 2015-01-27 15:48:07 -0800 | [diff] [blame] | 42 | // RegisterChild adds a new command to the registry that will be run |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 43 | // as a subprocess. It must be called before Dispatch or DispatchInTest is |
| 44 | // called, typically from an init function. |
| 45 | func RegisterChild(name, help string, main Main) { |
| 46 | factory := func() command { return newExecHandle(name) } |
| 47 | registry.addCommand(name, help, factory, main) |
| 48 | } |
| 49 | |
| 50 | // RegisterFunction adds a new command to the registry that will be run |
| 51 | // within the current process. It can be called at any time prior to an |
| 52 | // attempt to use it. |
| 53 | func RegisterFunction(name, help string, main Main) { |
| 54 | factory := func() command { return newFunctionHandle(name, main) } |
| 55 | registry.addCommand(name, help, factory, main) |
| 56 | } |
| 57 | |
| 58 | // Help returns the help message for the specified command, or a list |
| 59 | // of all commands if the command parameter is an empty string. |
| 60 | func Help(command string) string { |
| 61 | return registry.help(command) |
| 62 | } |
| 63 | |
| 64 | func (r *cmdRegistry) help(command string) string { |
| 65 | r.Lock() |
| 66 | defer r.Unlock() |
| 67 | if len(command) == 0 { |
| 68 | h := "" |
| 69 | for c, _ := range r.cmds { |
| 70 | h += c + ", " |
| 71 | } |
| 72 | return strings.TrimRight(h, ", ") |
| 73 | } |
| 74 | if c := r.cmds[command]; c != nil { |
| 75 | return command + ": " + c.help |
| 76 | } |
| 77 | return "" |
| 78 | } |
| 79 | |
| 80 | const shellEntryPoint = "VEYRON_SHELL_HELPER_PROCESS_ENTRY_POINT" |
| 81 | |
| 82 | // IsModulesProcess returns true if this process is run using |
| 83 | // the modules package. |
| 84 | func IsModulesProcess() bool { |
| 85 | return os.Getenv(shellEntryPoint) != "" |
| 86 | } |
| 87 | |
| 88 | // SetEntryPoint returns a slice of strings that is guaranteed to contain |
| 89 | // one and only instance of the entry point environment variable set to the |
| 90 | // supplied name value. |
| 91 | func SetEntryPoint(env map[string]string, name string) []string { |
| 92 | newEnv := make([]string, 0, len(env)+1) |
| 93 | for k, v := range env { |
| 94 | if k == shellEntryPoint { |
| 95 | continue |
| 96 | } |
| 97 | newEnv = append(newEnv, k+"="+v) |
| 98 | } |
| 99 | return append(newEnv, shellEntryPoint+"="+name) |
| 100 | } |
| 101 | |
| 102 | // DispatchInTest will execute the requested subproccess command from within |
| 103 | // a unit test run as a subprocess. |
| 104 | func DispatchInTest() { |
| 105 | if !IsTestHelperProcess() { |
| 106 | return |
| 107 | } |
| 108 | if err := registry.dispatch(); err != nil { |
| 109 | vlog.Fatalf("Failed: %s", err) |
| 110 | } |
| 111 | os.Exit(0) |
| 112 | } |
| 113 | |
| 114 | // Dispatch will execute the requested subprocess command from a within a |
| 115 | // a subprocess that is not a unit test. It will return without an error |
| 116 | // if it is executed by a process that does not specify an entry point |
| 117 | // in its environment. |
Cosmos Nicolaou | 90610bd | 2014-12-02 22:31:04 -0800 | [diff] [blame] | 118 | // |
| 119 | // func main() { |
| 120 | // if modules.IsModulesProcess() { |
| 121 | // if err := modules.Dispatch(); err != nil { |
| 122 | // panic("error") |
| 123 | // } |
| 124 | // return |
| 125 | // } |
| 126 | // parent code... |
| 127 | // |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 128 | func Dispatch() error { |
| 129 | if !IsModulesProcess() { |
| 130 | return nil |
| 131 | } |
| 132 | if IsTestHelperProcess() { |
| 133 | return fmt.Errorf("use DispatchInTest in unittests") |
| 134 | } |
| 135 | return registry.dispatch() |
| 136 | } |
| 137 | |
Cosmos Nicolaou | 90610bd | 2014-12-02 22:31:04 -0800 | [diff] [blame] | 138 | // DispatchAndExit is like Dispatch except that it will call os.Exit(0) |
| 139 | // when executed within a child process and the command succeeds, or panic |
| 140 | // on encountering an error. |
| 141 | // |
| 142 | // func main() { |
| 143 | // modules.DispatchAndExit() |
| 144 | // parent code... |
| 145 | // |
| 146 | func DispatchAndExit() { |
| 147 | if !IsModulesProcess() { |
| 148 | return |
| 149 | } |
| 150 | if IsTestHelperProcess() { |
| 151 | panic("use DispatchInTest in unittests") |
| 152 | } |
| 153 | if err := registry.dispatch(); err != nil { |
| 154 | panic(fmt.Sprintf("unexpected error: %s", err)) |
| 155 | } |
| 156 | os.Exit(0) |
| 157 | } |
| 158 | |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 159 | func (r *cmdRegistry) dispatch() error { |
| 160 | ch, err := vexec.GetChildHandle() |
| 161 | if err != nil { |
| 162 | // This is for debugging only. It's perfectly reasonable for this |
| 163 | // error to occur if the process is started by a means other |
| 164 | // than the exec library. |
| 165 | vlog.VI(1).Infof("failed to get child handle: %s", err) |
| 166 | } |
| 167 | |
| 168 | // Only signal that the child is ready or failed if we successfully get |
| 169 | // a child handle. We most likely failed to get a child handle |
| 170 | // because the subprocess was run directly from the command line. |
| 171 | command := os.Getenv(shellEntryPoint) |
| 172 | if len(command) == 0 { |
| 173 | err := fmt.Errorf("Failed to find entrypoint %q", command) |
| 174 | if ch != nil { |
| 175 | ch.SetFailed(err) |
| 176 | } |
| 177 | return err |
| 178 | } |
| 179 | |
| 180 | m := registry.getCommand(command) |
| 181 | if m == nil { |
| 182 | err := fmt.Errorf("%s: not registered", command) |
| 183 | if ch != nil { |
| 184 | ch.SetFailed(err) |
| 185 | } |
| 186 | return err |
| 187 | } |
| 188 | |
| 189 | if ch != nil { |
| 190 | ch.SetReady() |
| 191 | } |
| 192 | |
| 193 | go func(pid int) { |
| 194 | for { |
| 195 | _, err := os.FindProcess(pid) |
| 196 | if err != nil { |
| 197 | vlog.Fatalf("Looks like our parent exited: %v", err) |
| 198 | } |
| 199 | time.Sleep(time.Second) |
| 200 | } |
| 201 | }(os.Getppid()) |
| 202 | |
| 203 | args := append([]string{command}, flag.Args()...) |
| 204 | return m.main(os.Stdin, os.Stdout, os.Stderr, envSliceToMap(os.Environ()), args...) |
| 205 | } |
| 206 | |
Bogdan Caprita | c7e72b6 | 2015-01-07 19:22:23 -0800 | [diff] [blame] | 207 | // WaitForEOF returns when a read on its io.Reader parameter returns io.EOF |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 208 | func WaitForEOF(stdin io.Reader) { |
| 209 | buf := [1024]byte{} |
| 210 | for { |
| 211 | if _, err := stdin.Read(buf[:]); err == io.EOF { |
| 212 | return |
| 213 | } |
| 214 | } |
| 215 | } |