github.com/leowmjw/otto@v0.2.1-0.20160126165905-6400716cf085/plugin/client.go (about) 1 package plugin 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "net" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "sync" 16 "time" 17 "unicode" 18 19 pluginrpc "github.com/hashicorp/otto/rpc" 20 ) 21 22 // If this is true, then the "unexpected EOF" panic will not be 23 // raised throughout the clients. 24 var Killed = false 25 26 // This is a slice of the "managed" clients which are cleaned up when 27 // calling Cleanup 28 var managedClients = make([]*Client, 0, 5) 29 30 // Client handles the lifecycle of a plugin application, determining its 31 // RPC address, and returning various types of interface implementations 32 // across the multi-process communication layer. 33 type Client struct { 34 config *ClientConfig 35 exited bool 36 doneLogging chan struct{} 37 l sync.Mutex 38 address net.Addr 39 client *pluginrpc.Client 40 } 41 42 // ClientConfig is the configuration used to initialize a new 43 // plugin client. After being used to initialize a plugin client, 44 // that configuration must not be modified again. 45 type ClientConfig struct { 46 // The unstarted subprocess for starting the plugin. 47 Cmd *exec.Cmd 48 49 // Managed represents if the client should be managed by the 50 // plugin package or not. If true, then by calling CleanupClients, 51 // it will automatically be cleaned up. Otherwise, the client 52 // user is fully responsible for making sure to Kill all plugin 53 // clients. By default the client is _not_ managed. 54 Managed bool 55 56 // The minimum and maximum port to use for communicating with 57 // the subprocess. If not set, this defaults to 10,000 and 25,000 58 // respectively. 59 MinPort, MaxPort uint 60 61 // StartTimeout is the timeout to wait for the plugin to say it 62 // has started successfully. 63 StartTimeout time.Duration 64 65 // If non-nil, then the stderr of the client will be written to here 66 // (as well as the log). This is the original os.Stderr of the subprocess. 67 // This isn't the output of synced stderr. 68 Stderr io.Writer 69 70 // SyncStdout, SyncStderr can be set to override the 71 // respective os.Std* values in the plugin. Care should be taken to 72 // avoid races here. If these are nil, then this will automatically be 73 // hooked up to os.Stdin, Stdout, and Stderr, respectively. 74 // 75 // If the default values (nil) are used, then this package will not 76 // sync any of these streams. 77 SyncStdout io.Writer 78 SyncStderr io.Writer 79 } 80 81 // This makes sure all the managed subprocesses are killed and properly 82 // logged. This should be called before the parent process running the 83 // plugins exits. 84 // 85 // This must only be called _once_. 86 func CleanupClients() { 87 // Set the killed to true so that we don't get unexpected panics 88 Killed = true 89 90 // Kill all the managed clients in parallel and use a WaitGroup 91 // to wait for them all to finish up. 92 var wg sync.WaitGroup 93 for _, client := range managedClients { 94 wg.Add(1) 95 96 go func(client *Client) { 97 client.Kill() 98 wg.Done() 99 }(client) 100 } 101 102 log.Println("[DEBUG] waiting for all plugin processes to complete...") 103 wg.Wait() 104 } 105 106 // Creates a new plugin client which manages the lifecycle of an external 107 // plugin and gets the address for the RPC connection. 108 // 109 // The client must be cleaned up at some point by calling Kill(). If 110 // the client is a managed client (created with NewManagedClient) you 111 // can just call CleanupClients at the end of your program and they will 112 // be properly cleaned. 113 func NewClient(config *ClientConfig) (c *Client) { 114 if config.MinPort == 0 && config.MaxPort == 0 { 115 config.MinPort = 10000 116 config.MaxPort = 25000 117 } 118 119 if config.StartTimeout == 0 { 120 config.StartTimeout = 1 * time.Minute 121 } 122 123 if config.Stderr == nil { 124 config.Stderr = ioutil.Discard 125 } 126 127 if config.SyncStdout == nil { 128 config.SyncStdout = ioutil.Discard 129 } 130 if config.SyncStderr == nil { 131 config.SyncStderr = ioutil.Discard 132 } 133 134 c = &Client{config: config} 135 if config.Managed { 136 managedClients = append(managedClients, c) 137 } 138 139 return 140 } 141 142 // Client returns an RPC client for the plugin. 143 // 144 // Subsequent calls to this will return the same RPC client. 145 func (c *Client) Client() (*pluginrpc.Client, error) { 146 addr, err := c.Start() 147 if err != nil { 148 return nil, err 149 } 150 151 c.l.Lock() 152 defer c.l.Unlock() 153 154 if c.client != nil { 155 return c.client, nil 156 } 157 158 c.client, err = pluginrpc.Dial(addr.Network(), addr.String()) 159 if err != nil { 160 return nil, err 161 } 162 163 // Begin the stream syncing so that stdin, out, err work properly 164 err = c.client.SyncStreams( 165 c.config.SyncStdout, 166 c.config.SyncStderr) 167 if err != nil { 168 c.client.Close() 169 c.client = nil 170 return nil, err 171 } 172 173 return c.client, nil 174 } 175 176 // Tells whether or not the underlying process has exited. 177 func (c *Client) Exited() bool { 178 c.l.Lock() 179 defer c.l.Unlock() 180 return c.exited 181 } 182 183 // End the executing subprocess (if it is running) and perform any cleanup 184 // tasks necessary such as capturing any remaining logs and so on. 185 // 186 // This method blocks until the process successfully exits. 187 // 188 // This method can safely be called multiple times. 189 func (c *Client) Kill() { 190 cmd := c.config.Cmd 191 192 if cmd.Process == nil { 193 return 194 } 195 196 cmd.Process.Kill() 197 198 // Wait for the client to finish logging so we have a complete log 199 <-c.doneLogging 200 } 201 202 // Starts the underlying subprocess, communicating with it to negotiate 203 // a port for RPC connections, and returning the address to connect via RPC. 204 // 205 // This method is safe to call multiple times. Subsequent calls have no effect. 206 // Once a client has been started once, it cannot be started again, even if 207 // it was killed. 208 func (c *Client) Start() (addr net.Addr, err error) { 209 c.l.Lock() 210 defer c.l.Unlock() 211 212 if c.address != nil { 213 return c.address, nil 214 } 215 216 c.doneLogging = make(chan struct{}) 217 218 env := []string{ 219 fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue), 220 fmt.Sprintf("OTTO_PLUGIN_MIN_PORT=%d", c.config.MinPort), 221 fmt.Sprintf("OTTO_PLUGIN_MAX_PORT=%d", c.config.MaxPort), 222 } 223 224 stdout_r, stdout_w := io.Pipe() 225 stderr_r, stderr_w := io.Pipe() 226 227 cmd := c.config.Cmd 228 cmd.Env = append(cmd.Env, os.Environ()...) 229 cmd.Env = append(cmd.Env, env...) 230 cmd.Stdin = os.Stdin 231 cmd.Stderr = stderr_w 232 cmd.Stdout = stdout_w 233 234 log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args) 235 err = cmd.Start() 236 if err != nil { 237 return 238 } 239 240 // Make sure the command is properly cleaned up if there is an error 241 defer func() { 242 r := recover() 243 244 if err != nil || r != nil { 245 cmd.Process.Kill() 246 } 247 248 if r != nil { 249 panic(r) 250 } 251 }() 252 253 // Start goroutine to wait for process to exit 254 exitCh := make(chan struct{}) 255 go func() { 256 // Make sure we close the write end of our stderr/stdout so 257 // that the readers send EOF properly. 258 defer stderr_w.Close() 259 defer stdout_w.Close() 260 261 // Wait for the command to end. 262 cmd.Wait() 263 264 // Log and make sure to flush the logs write away 265 log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path) 266 os.Stderr.Sync() 267 268 // Mark that we exited 269 close(exitCh) 270 271 // Set that we exited, which takes a lock 272 c.l.Lock() 273 defer c.l.Unlock() 274 c.exited = true 275 }() 276 277 // Start goroutine that logs the stderr 278 go c.logStderr(stderr_r) 279 280 // Start a goroutine that is going to be reading the lines 281 // out of stdout 282 linesCh := make(chan []byte) 283 go func() { 284 defer close(linesCh) 285 286 buf := bufio.NewReader(stdout_r) 287 for { 288 line, err := buf.ReadBytes('\n') 289 if line != nil { 290 linesCh <- line 291 } 292 293 if err == io.EOF { 294 return 295 } 296 } 297 }() 298 299 // Make sure after we exit we read the lines from stdout forever 300 // so they don't block since it is an io.Pipe 301 defer func() { 302 go func() { 303 for _ = range linesCh { 304 } 305 }() 306 }() 307 308 // Some channels for the next step 309 timeout := time.After(c.config.StartTimeout) 310 311 // Start looking for the address 312 log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path) 313 select { 314 case <-timeout: 315 err = errors.New("timeout while waiting for plugin to start") 316 case <-exitCh: 317 err = errors.New("plugin exited before we could connect") 318 case lineBytes := <-linesCh: 319 // Trim the line and split by "|" in order to get the parts of 320 // the output. 321 line := strings.TrimSpace(string(lineBytes)) 322 parts := strings.SplitN(line, "|", 3) 323 if len(parts) < 3 { 324 err = fmt.Errorf("Unrecognized remote plugin message: %s", line) 325 return 326 } 327 328 // Test the API version 329 if parts[0] != APIVersion { 330 err = fmt.Errorf("Incompatible API version with plugin. "+ 331 "Plugin version: %s, Ours: %s", parts[0], APIVersion) 332 return 333 } 334 335 switch parts[1] { 336 case "tcp": 337 addr, err = net.ResolveTCPAddr("tcp", parts[2]) 338 case "unix": 339 addr, err = net.ResolveUnixAddr("unix", parts[2]) 340 default: 341 err = fmt.Errorf("Unknown address type: %s", parts[1]) 342 } 343 } 344 345 c.address = addr 346 return 347 } 348 349 func (c *Client) logStderr(r io.Reader) { 350 bufR := bufio.NewReader(r) 351 for { 352 line, err := bufR.ReadString('\n') 353 if line != "" { 354 c.config.Stderr.Write([]byte(line)) 355 356 line = strings.TrimRightFunc(line, unicode.IsSpace) 357 log.Printf("[DEBUG] %s: %s", filepath.Base(c.config.Cmd.Path), line) 358 } 359 360 if err == io.EOF { 361 break 362 } 363 } 364 365 // Flag that we've completed logging for others 366 close(c.doneLogging) 367 }