123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- /*
- Package gexec provides support for testing external processes.
- */
- package gexec
- import (
- "io"
- "os"
- "os/exec"
- "sync"
- "syscall"
- . "github.com/onsi/gomega"
- "github.com/onsi/gomega/gbytes"
- )
- const INVALID_EXIT_CODE = 254
- type Session struct {
- //The wrapped command
- Command *exec.Cmd
- //A *gbytes.Buffer connected to the command's stdout
- Out *gbytes.Buffer
- //A *gbytes.Buffer connected to the command's stderr
- Err *gbytes.Buffer
- //A channel that will close when the command exits
- Exited <-chan struct{}
- lock *sync.Mutex
- exitCode int
- }
- /*
- Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session.
- The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err.
- These buffers can be used with the gbytes.Say matcher to match against unread output:
- Expect(session.Out).Should(gbytes.Say("foo-out"))
- Expect(session.Err).Should(gbytes.Say("foo-err"))
- In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with:
- Expect(session).Should(gbytes.Say("foo-out"))
- When outWriter and/or errWriter are non-nil, the session will pipe stdout and/or stderr output both into the session *gybtes.Buffers and to the passed-in outWriter/errWriter.
- This is useful for capturing the process's output or logging it to screen. In particular, when using Ginkgo it can be convenient to direct output to the GinkgoWriter:
- session, err := Start(command, GinkgoWriter, GinkgoWriter)
- This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails.
- The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself.
- Instead, to assert that the command has exited you can use the gexec.Exit matcher:
- Expect(session).Should(gexec.Exit())
- When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any
- Eventuallys waiting for the buffers to Say something.
- */
- func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) {
- exited := make(chan struct{})
- session := &Session{
- Command: command,
- Out: gbytes.NewBuffer(),
- Err: gbytes.NewBuffer(),
- Exited: exited,
- lock: &sync.Mutex{},
- exitCode: -1,
- }
- var commandOut, commandErr io.Writer
- commandOut, commandErr = session.Out, session.Err
- if outWriter != nil {
- commandOut = io.MultiWriter(commandOut, outWriter)
- }
- if errWriter != nil {
- commandErr = io.MultiWriter(commandErr, errWriter)
- }
- command.Stdout = commandOut
- command.Stderr = commandErr
- err := command.Start()
- if err == nil {
- go session.monitorForExit(exited)
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- trackedSessions = append(trackedSessions, session)
- }
- return session, err
- }
- /*
- Buffer implements the gbytes.BufferProvider interface and returns s.Out
- This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out:
- Eventually(session).Should(gbytes.Say("foo"))
- */
- func (s *Session) Buffer() *gbytes.Buffer {
- return s.Out
- }
- /*
- ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1.
- To assert that the command has exited it is more convenient to use the Exit matcher:
- Eventually(s).Should(gexec.Exit())
- When the process exits because it has received a particular signal, the exit code will be 128+signal-value
- (See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html)
- */
- func (s *Session) ExitCode() int {
- s.lock.Lock()
- defer s.lock.Unlock()
- return s.exitCode
- }
- /*
- Wait waits until the wrapped command exits. It can be passed an optional timeout.
- If the command does not exit within the timeout, Wait will trigger a test failure.
- Wait returns the session, making it possible to chain:
- session.Wait().Out.Contents()
- will wait for the command to exit then return the entirety of Out's contents.
- Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does.
- */
- func (s *Session) Wait(timeout ...interface{}) *Session {
- EventuallyWithOffset(1, s, timeout...).Should(Exit())
- return s
- }
- /*
- Kill sends the running command a SIGKILL signal. It does not wait for the process to exit.
- If the command has already exited, Kill returns silently.
- The session is returned to enable chaining.
- */
- func (s *Session) Kill() *Session {
- return s.Signal(syscall.SIGKILL)
- }
- /*
- Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit.
- If the command has already exited, Interrupt returns silently.
- The session is returned to enable chaining.
- */
- func (s *Session) Interrupt() *Session {
- return s.Signal(syscall.SIGINT)
- }
- /*
- Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit.
- If the command has already exited, Terminate returns silently.
- The session is returned to enable chaining.
- */
- func (s *Session) Terminate() *Session {
- return s.Signal(syscall.SIGTERM)
- }
- /*
- Signal sends the running command the passed in signal. It does not wait for the process to exit.
- If the command has already exited, Signal returns silently.
- The session is returned to enable chaining.
- */
- func (s *Session) Signal(signal os.Signal) *Session {
- if s.processIsAlive() {
- s.Command.Process.Signal(signal)
- }
- return s
- }
- func (s *Session) monitorForExit(exited chan<- struct{}) {
- err := s.Command.Wait()
- s.lock.Lock()
- s.Out.Close()
- s.Err.Close()
- status := s.Command.ProcessState.Sys().(syscall.WaitStatus)
- if status.Signaled() {
- s.exitCode = 128 + int(status.Signal())
- } else {
- exitStatus := status.ExitStatus()
- if exitStatus == -1 && err != nil {
- s.exitCode = INVALID_EXIT_CODE
- }
- s.exitCode = exitStatus
- }
- s.lock.Unlock()
- close(exited)
- }
- func (s *Session) processIsAlive() bool {
- return s.ExitCode() == -1 && s.Command.Process != nil
- }
- var trackedSessions = []*Session{}
- var trackedSessionsMutex = &sync.Mutex{}
- /*
- Kill sends a SIGKILL signal to all the processes started by Run, and waits for them to exit.
- The timeout specified is applied to each process killed.
- If any of the processes already exited, KillAndWait returns silently.
- */
- func KillAndWait(timeout ...interface{}) {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Kill().Wait(timeout...)
- }
- trackedSessions = []*Session{}
- }
- /*
- Kill sends a SIGTERM signal to all the processes started by Run, and waits for them to exit.
- The timeout specified is applied to each process killed.
- If any of the processes already exited, TerminateAndWait returns silently.
- */
- func TerminateAndWait(timeout ...interface{}) {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Terminate().Wait(timeout...)
- }
- }
- /*
- Kill sends a SIGKILL signal to all the processes started by Run.
- It does not wait for the processes to exit.
- If any of the processes already exited, Kill returns silently.
- */
- func Kill() {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Kill()
- }
- }
- /*
- Terminate sends a SIGTERM signal to all the processes started by Run.
- It does not wait for the processes to exit.
- If any of the processes already exited, Terminate returns silently.
- */
- func Terminate() {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Terminate()
- }
- }
- /*
- Signal sends the passed in signal to all the processes started by Run.
- It does not wait for the processes to exit.
- If any of the processes already exited, Signal returns silently.
- */
- func Signal(signal os.Signal) {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Signal(signal)
- }
- }
- /*
- Interrupt sends the SIGINT signal to all the processes started by Run.
- It does not wait for the processes to exit.
- If any of the processes already exited, Interrupt returns silently.
- */
- func Interrupt() {
- trackedSessionsMutex.Lock()
- defer trackedSessionsMutex.Unlock()
- for _, session := range trackedSessions {
- session.Interrupt()
- }
- }
|