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