| // Copyright 2020 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| package fuzz | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "time" | |
| "github.com/golang/glog" | |
| "golang.org/x/crypto/ssh" | |
| ) | |
| // TODO(fxbug.dev/45424): use timeout for all methods | |
| // An InstanceCmd represents a remote command to be run on an Instance | |
| // This interface is similar to that of os.exec.Cmd | |
| type InstanceCmd interface { | |
| // Output runs the command and returns its combined output. | |
| Output() ([]byte, error) | |
| // Runs the specified command and waits for it to complete. | |
| // | |
| // The returned error is nil if the command runs, has no problems copying | |
| // input and output, and exits with a zero exit status. | |
| Run() error | |
| // Start starts the specified command but does not wait for it to complete. | |
| Start() error | |
| // StdinPipe returns a pipe that will be connected to the command's input | |
| // when the command starts. The pipe will be closed automatically after | |
| // Wait sees the command exit. A caller need only call Close to force the | |
| // pipe to close sooner. For example, if the command being run will not | |
| // exit until the input is closed, the caller must close the pipe. | |
| StdinPipe() (io.WriteCloser, error) | |
| // StdoutPipe returns a pipe that will be connected to the command's output | |
| // when the command starts. | |
| // | |
| // Wait will close the pipe after seeing the command exit, so most callers | |
| // need not close the pipe themselves; however, an implication is that it | |
| // is incorrect to call Wait before all reads from the pipe have completed. | |
| // For the same reason, it is incorrect to call Run when using StdoutPipe. | |
| StdoutPipe() (io.ReadCloser, error) | |
| // Wait waits for the command to exit and waits for any copying | |
| // to input or from output to complete. | |
| // | |
| // The command must have been started by Start. | |
| // | |
| // The returned error is nil if the command runs, has no problems | |
| // copying input and output, and exits with a zero exit status | |
| // before any timeout set with SetTimeout is triggered. | |
| // | |
| // Wait releases any resources associated with the InstanceCmd. | |
| Wait() error | |
| // SetTimeout sets a timeout that will be used for all blocking | |
| // methods. If a method takes longer than the timeout duration to | |
| // complete, it will return early with an error. If not set, or | |
| // set to 0, methods will block forever. | |
| SetTimeout(duration time.Duration) | |
| // Kill forcefully terminates the remote command | |
| Kill() error | |
| } | |
| // A SSHInstanceCmd represents a command to be run over SSH | |
| type SSHInstanceCmd struct { | |
| connector *SSHConnector | |
| cmdline string | |
| pid int | |
| input io.ReadCloser | |
| output io.Writer | |
| errlog bytes.Buffer | |
| session *ssh.Session | |
| timeout time.Duration | |
| } | |
| // Output executes the command and returns its output | |
| func (c *SSHInstanceCmd) Output() ([]byte, error) { | |
| var buf bytes.Buffer | |
| c.output = &buf | |
| if err := c.Run(); err != nil { | |
| return nil, err | |
| } | |
| return buf.Bytes(), nil | |
| } | |
| // Run runs the command to completion | |
| func (c *SSHInstanceCmd) Run() error { | |
| if err := c.Start(); err != nil { | |
| return err | |
| } | |
| return c.Wait() | |
| } | |
| // Start the command, but don't wait for it to complete | |
| func (c *SSHInstanceCmd) Start() error { | |
| if c.pid != 0 { | |
| return fmt.Errorf("start called on already-running command") | |
| } | |
| if c.connector.client == nil { | |
| if err := c.connector.Connect(); err != nil { | |
| return err | |
| } | |
| } | |
| session, err := c.connector.client.NewSession() | |
| if err != nil { | |
| return fmt.Errorf("error starting ssh session: %s", err) | |
| } | |
| glog.Infof("Running ssh command: %s", c.cmdline) | |
| session.Stdin = c.input | |
| session.Stdout = c.output | |
| if c.output != nil { | |
| // TODO(fxbug.dev/45424): limit size of errlog, we only really want the tail for debugging | |
| session.Stderr = io.MultiWriter(c.output, &c.errlog) | |
| } else { | |
| session.Stderr = &c.errlog | |
| } | |
| if err := session.Start(c.cmdline); err != nil { | |
| return err | |
| } | |
| c.session = session // TODO(fxbug.dev/45424): clean up | |
| return nil | |
| } | |
| // StdinPipe returns a pipe connected to the command's input | |
| func (c *SSHInstanceCmd) StdinPipe() (io.WriteCloser, error) { | |
| r, w := io.Pipe() | |
| c.input = r | |
| return w, nil | |
| } | |
| // StdoutPipe returns a pipe connected to the command's output | |
| // TODO(fxbug.dev/45424): this currently includes stderr in addition to stdout | |
| // TODO(fxbug.dev/45424): use ssh.Session's implementation of pipe stuff, etc | |
| func (c *SSHInstanceCmd) StdoutPipe() (io.ReadCloser, error) { | |
| r, w := io.Pipe() | |
| c.output = w | |
| return r, nil | |
| } | |
| // Kill sends a KILL signal to the remote process | |
| func (c *SSHInstanceCmd) Kill() error { | |
| return c.session.Signal(ssh.SIGKILL) | |
| } | |
| // SetTimeout sets the global timeout | |
| func (c *SSHInstanceCmd) SetTimeout(duration time.Duration) { | |
| c.timeout = duration | |
| } | |
| // Wait for the remote command to complete | |
| func (c *SSHInstanceCmd) Wait() error { | |
| defer c.session.Close() | |
| // On quit, close stdout in case someone is blocking on us | |
| if wc, ok := c.output.(io.WriteCloser); ok { | |
| defer wc.Close() | |
| } | |
| errs := make(chan error) | |
| go func() { errs <- c.session.Wait() }() | |
| var timeoutCh <-chan time.Time | |
| if c.timeout != 0 { | |
| timeoutCh = time.After(c.timeout) | |
| } | |
| select { | |
| case err := <-errs: | |
| if err == nil { | |
| return nil | |
| } | |
| if cmderr, ok := err.(*ssh.ExitError); ok { | |
| return &InstanceCmdError{ | |
| ReturnCode: cmderr.ExitStatus(), | |
| Command: c.cmdline, | |
| Stderr: c.errlog.String(), | |
| } | |
| } | |
| return err | |
| case <-timeoutCh: | |
| // TODO(fxbug.dev/45424): clean up the ssh command | |
| return fmt.Errorf("timeout waiting for command to complete") | |
| } | |
| } | |
| // InstanceCmdError includes extra information when commands fail | |
| type InstanceCmdError struct { | |
| ReturnCode int | |
| Command string | |
| Stderr string | |
| } | |
| func (e *InstanceCmdError) Error() string { | |
| return fmt.Sprintf("'%s' exited with error: %d (%s)", e.Command, e.ReturnCode, e.Stderr) | |
| } |