session.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. Package gexec provides support for testing external processes.
  3. */
  4. package gexec
  5. import (
  6. "io"
  7. "os"
  8. "os/exec"
  9. "sync"
  10. "syscall"
  11. . "github.com/onsi/gomega"
  12. "github.com/onsi/gomega/gbytes"
  13. )
  14. const INVALID_EXIT_CODE = 254
  15. type Session struct {
  16. //The wrapped command
  17. Command *exec.Cmd
  18. //A *gbytes.Buffer connected to the command's stdout
  19. Out *gbytes.Buffer
  20. //A *gbytes.Buffer connected to the command's stderr
  21. Err *gbytes.Buffer
  22. //A channel that will close when the command exits
  23. Exited <-chan struct{}
  24. lock *sync.Mutex
  25. exitCode int
  26. }
  27. /*
  28. Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session.
  29. The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err.
  30. These buffers can be used with the gbytes.Say matcher to match against unread output:
  31. Expect(session.Out).Should(gbytes.Say("foo-out"))
  32. Expect(session.Err).Should(gbytes.Say("foo-err"))
  33. In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with:
  34. Expect(session).Should(gbytes.Say("foo-out"))
  35. 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.
  36. 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:
  37. session, err := Start(command, GinkgoWriter, GinkgoWriter)
  38. This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails.
  39. The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself.
  40. Instead, to assert that the command has exited you can use the gexec.Exit matcher:
  41. Expect(session).Should(gexec.Exit())
  42. When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any
  43. Eventuallys waiting for the buffers to Say something.
  44. */
  45. func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) {
  46. exited := make(chan struct{})
  47. session := &Session{
  48. Command: command,
  49. Out: gbytes.NewBuffer(),
  50. Err: gbytes.NewBuffer(),
  51. Exited: exited,
  52. lock: &sync.Mutex{},
  53. exitCode: -1,
  54. }
  55. var commandOut, commandErr io.Writer
  56. commandOut, commandErr = session.Out, session.Err
  57. if outWriter != nil {
  58. commandOut = io.MultiWriter(commandOut, outWriter)
  59. }
  60. if errWriter != nil {
  61. commandErr = io.MultiWriter(commandErr, errWriter)
  62. }
  63. command.Stdout = commandOut
  64. command.Stderr = commandErr
  65. err := command.Start()
  66. if err == nil {
  67. go session.monitorForExit(exited)
  68. trackedSessionsMutex.Lock()
  69. defer trackedSessionsMutex.Unlock()
  70. trackedSessions = append(trackedSessions, session)
  71. }
  72. return session, err
  73. }
  74. /*
  75. Buffer implements the gbytes.BufferProvider interface and returns s.Out
  76. This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out:
  77. Eventually(session).Should(gbytes.Say("foo"))
  78. */
  79. func (s *Session) Buffer() *gbytes.Buffer {
  80. return s.Out
  81. }
  82. /*
  83. ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1.
  84. To assert that the command has exited it is more convenient to use the Exit matcher:
  85. Eventually(s).Should(gexec.Exit())
  86. When the process exits because it has received a particular signal, the exit code will be 128+signal-value
  87. (See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html)
  88. */
  89. func (s *Session) ExitCode() int {
  90. s.lock.Lock()
  91. defer s.lock.Unlock()
  92. return s.exitCode
  93. }
  94. /*
  95. Wait waits until the wrapped command exits. It can be passed an optional timeout.
  96. If the command does not exit within the timeout, Wait will trigger a test failure.
  97. Wait returns the session, making it possible to chain:
  98. session.Wait().Out.Contents()
  99. will wait for the command to exit then return the entirety of Out's contents.
  100. Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does.
  101. */
  102. func (s *Session) Wait(timeout ...interface{}) *Session {
  103. EventuallyWithOffset(1, s, timeout...).Should(Exit())
  104. return s
  105. }
  106. /*
  107. Kill sends the running command a SIGKILL signal. It does not wait for the process to exit.
  108. If the command has already exited, Kill returns silently.
  109. The session is returned to enable chaining.
  110. */
  111. func (s *Session) Kill() *Session {
  112. return s.Signal(syscall.SIGKILL)
  113. }
  114. /*
  115. Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit.
  116. If the command has already exited, Interrupt returns silently.
  117. The session is returned to enable chaining.
  118. */
  119. func (s *Session) Interrupt() *Session {
  120. return s.Signal(syscall.SIGINT)
  121. }
  122. /*
  123. Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit.
  124. If the command has already exited, Terminate returns silently.
  125. The session is returned to enable chaining.
  126. */
  127. func (s *Session) Terminate() *Session {
  128. return s.Signal(syscall.SIGTERM)
  129. }
  130. /*
  131. Signal sends the running command the passed in signal. It does not wait for the process to exit.
  132. If the command has already exited, Signal returns silently.
  133. The session is returned to enable chaining.
  134. */
  135. func (s *Session) Signal(signal os.Signal) *Session {
  136. if s.processIsAlive() {
  137. s.Command.Process.Signal(signal)
  138. }
  139. return s
  140. }
  141. func (s *Session) monitorForExit(exited chan<- struct{}) {
  142. err := s.Command.Wait()
  143. s.lock.Lock()
  144. s.Out.Close()
  145. s.Err.Close()
  146. status := s.Command.ProcessState.Sys().(syscall.WaitStatus)
  147. if status.Signaled() {
  148. s.exitCode = 128 + int(status.Signal())
  149. } else {
  150. exitStatus := status.ExitStatus()
  151. if exitStatus == -1 && err != nil {
  152. s.exitCode = INVALID_EXIT_CODE
  153. }
  154. s.exitCode = exitStatus
  155. }
  156. s.lock.Unlock()
  157. close(exited)
  158. }
  159. func (s *Session) processIsAlive() bool {
  160. return s.ExitCode() == -1 && s.Command.Process != nil
  161. }
  162. var trackedSessions = []*Session{}
  163. var trackedSessionsMutex = &sync.Mutex{}
  164. /*
  165. Kill sends a SIGKILL signal to all the processes started by Run, and waits for them to exit.
  166. The timeout specified is applied to each process killed.
  167. If any of the processes already exited, KillAndWait returns silently.
  168. */
  169. func KillAndWait(timeout ...interface{}) {
  170. trackedSessionsMutex.Lock()
  171. defer trackedSessionsMutex.Unlock()
  172. for _, session := range trackedSessions {
  173. session.Kill().Wait(timeout...)
  174. }
  175. trackedSessions = []*Session{}
  176. }
  177. /*
  178. Kill sends a SIGTERM signal to all the processes started by Run, and waits for them to exit.
  179. The timeout specified is applied to each process killed.
  180. If any of the processes already exited, TerminateAndWait returns silently.
  181. */
  182. func TerminateAndWait(timeout ...interface{}) {
  183. trackedSessionsMutex.Lock()
  184. defer trackedSessionsMutex.Unlock()
  185. for _, session := range trackedSessions {
  186. session.Terminate().Wait(timeout...)
  187. }
  188. }
  189. /*
  190. Kill sends a SIGKILL signal to all the processes started by Run.
  191. It does not wait for the processes to exit.
  192. If any of the processes already exited, Kill returns silently.
  193. */
  194. func Kill() {
  195. trackedSessionsMutex.Lock()
  196. defer trackedSessionsMutex.Unlock()
  197. for _, session := range trackedSessions {
  198. session.Kill()
  199. }
  200. }
  201. /*
  202. Terminate sends a SIGTERM signal to all the processes started by Run.
  203. It does not wait for the processes to exit.
  204. If any of the processes already exited, Terminate returns silently.
  205. */
  206. func Terminate() {
  207. trackedSessionsMutex.Lock()
  208. defer trackedSessionsMutex.Unlock()
  209. for _, session := range trackedSessions {
  210. session.Terminate()
  211. }
  212. }
  213. /*
  214. Signal sends the passed in signal to all the processes started by Run.
  215. It does not wait for the processes to exit.
  216. If any of the processes already exited, Signal returns silently.
  217. */
  218. func Signal(signal os.Signal) {
  219. trackedSessionsMutex.Lock()
  220. defer trackedSessionsMutex.Unlock()
  221. for _, session := range trackedSessions {
  222. session.Signal(signal)
  223. }
  224. }
  225. /*
  226. Interrupt sends the SIGINT signal to all the processes started by Run.
  227. It does not wait for the processes to exit.
  228. If any of the processes already exited, Interrupt returns silently.
  229. */
  230. func Interrupt() {
  231. trackedSessionsMutex.Lock()
  232. defer trackedSessionsMutex.Unlock()
  233. for _, session := range trackedSessions {
  234. session.Interrupt()
  235. }
  236. }