Path: blob/main/vendor/github.com/onsi/gomega/gexec/session.go
2880 views
/*1Package gexec provides support for testing external processes.2*/34// untested sections: 156package gexec78import (9"io"10"os"11"os/exec"12"sync"13"syscall"1415. "github.com/onsi/gomega"16"github.com/onsi/gomega/gbytes"17)1819const INVALID_EXIT_CODE = 2542021type Session struct {22//The wrapped command23Command *exec.Cmd2425//A *gbytes.Buffer connected to the command's stdout26Out *gbytes.Buffer2728//A *gbytes.Buffer connected to the command's stderr29Err *gbytes.Buffer3031//A channel that will close when the command exits32Exited <-chan struct{}3334lock *sync.Mutex35exitCode int36}3738/*39Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session.4041The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err.42These buffers can be used with the gbytes.Say matcher to match against unread output:4344Expect(session.Out).Should(gbytes.Say("foo-out"))45Expect(session.Err).Should(gbytes.Say("foo-err"))4647In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with:4849Expect(session).Should(gbytes.Say("foo-out"))5051When 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.52This 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:5354session, err := Start(command, GinkgoWriter, GinkgoWriter)5556This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails.5758The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself.59Instead, to assert that the command has exited you can use the gexec.Exit matcher:6061Expect(session).Should(gexec.Exit())6263When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any64Eventuallys waiting for the buffers to Say something.65*/66func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) {67exited := make(chan struct{})6869session := &Session{70Command: command,71Out: gbytes.NewBuffer(),72Err: gbytes.NewBuffer(),73Exited: exited,74lock: &sync.Mutex{},75exitCode: -1,76}7778var commandOut, commandErr io.Writer7980commandOut, commandErr = session.Out, session.Err8182if outWriter != nil {83commandOut = io.MultiWriter(commandOut, outWriter)84}8586if errWriter != nil {87commandErr = io.MultiWriter(commandErr, errWriter)88}8990command.Stdout = commandOut91command.Stderr = commandErr9293err := command.Start()94if err == nil {95go session.monitorForExit(exited)96trackedSessionsMutex.Lock()97defer trackedSessionsMutex.Unlock()98trackedSessions = append(trackedSessions, session)99}100101return session, err102}103104/*105Buffer implements the gbytes.BufferProvider interface and returns s.Out106This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out:107108Eventually(session).Should(gbytes.Say("foo"))109*/110func (s *Session) Buffer() *gbytes.Buffer {111return s.Out112}113114/*115ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1.116117To assert that the command has exited it is more convenient to use the Exit matcher:118119Eventually(s).Should(gexec.Exit())120121When the process exits because it has received a particular signal, the exit code will be 128+signal-value122(See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html)123*/124func (s *Session) ExitCode() int {125s.lock.Lock()126defer s.lock.Unlock()127return s.exitCode128}129130/*131Wait waits until the wrapped command exits. It can be passed an optional timeout.132If the command does not exit within the timeout, Wait will trigger a test failure.133134Wait returns the session, making it possible to chain:135136session.Wait().Out.Contents()137138will wait for the command to exit then return the entirety of Out's contents.139140Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does.141*/142func (s *Session) Wait(timeout ...any) *Session {143EventuallyWithOffset(1, s, timeout...).Should(Exit())144return s145}146147/*148Kill sends the running command a SIGKILL signal. It does not wait for the process to exit.149150If the command has already exited, Kill returns silently.151152The session is returned to enable chaining.153*/154func (s *Session) Kill() *Session {155return s.Signal(syscall.SIGKILL)156}157158/*159Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit.160161If the command has already exited, Interrupt returns silently.162163The session is returned to enable chaining.164*/165func (s *Session) Interrupt() *Session {166return s.Signal(syscall.SIGINT)167}168169/*170Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit.171172If the command has already exited, Terminate returns silently.173174The session is returned to enable chaining.175*/176func (s *Session) Terminate() *Session {177return s.Signal(syscall.SIGTERM)178}179180/*181Signal sends the running command the passed in signal. It does not wait for the process to exit.182183If the command has already exited, Signal returns silently.184185The session is returned to enable chaining.186*/187func (s *Session) Signal(signal os.Signal) *Session {188if s.processIsAlive() {189s.Command.Process.Signal(signal)190}191return s192}193194func (s *Session) monitorForExit(exited chan<- struct{}) {195err := s.Command.Wait()196s.lock.Lock()197s.Out.Close()198s.Err.Close()199status := s.Command.ProcessState.Sys().(syscall.WaitStatus)200if status.Signaled() {201s.exitCode = 128 + int(status.Signal())202} else {203exitStatus := status.ExitStatus()204if exitStatus == -1 && err != nil {205s.exitCode = INVALID_EXIT_CODE206}207s.exitCode = exitStatus208}209s.lock.Unlock()210211close(exited)212}213214func (s *Session) processIsAlive() bool {215return s.ExitCode() == -1 && s.Command.Process != nil216}217218var trackedSessions = []*Session{}219var trackedSessionsMutex = &sync.Mutex{}220221/*222Kill sends a SIGKILL signal to all the processes started by Run, and waits for them to exit.223The timeout specified is applied to each process killed.224225If any of the processes already exited, KillAndWait returns silently.226*/227func KillAndWait(timeout ...any) {228trackedSessionsMutex.Lock()229defer trackedSessionsMutex.Unlock()230for _, session := range trackedSessions {231session.Kill().Wait(timeout...)232}233trackedSessions = []*Session{}234}235236/*237Kill sends a SIGTERM signal to all the processes started by Run, and waits for them to exit.238The timeout specified is applied to each process killed.239240If any of the processes already exited, TerminateAndWait returns silently.241*/242func TerminateAndWait(timeout ...any) {243trackedSessionsMutex.Lock()244defer trackedSessionsMutex.Unlock()245for _, session := range trackedSessions {246session.Terminate().Wait(timeout...)247}248}249250/*251Kill sends a SIGKILL signal to all the processes started by Run.252It does not wait for the processes to exit.253254If any of the processes already exited, Kill returns silently.255*/256func Kill() {257trackedSessionsMutex.Lock()258defer trackedSessionsMutex.Unlock()259for _, session := range trackedSessions {260session.Kill()261}262}263264/*265Terminate sends a SIGTERM signal to all the processes started by Run.266It does not wait for the processes to exit.267268If any of the processes already exited, Terminate returns silently.269*/270func Terminate() {271trackedSessionsMutex.Lock()272defer trackedSessionsMutex.Unlock()273for _, session := range trackedSessions {274session.Terminate()275}276}277278/*279Signal sends the passed in signal to all the processes started by Run.280It does not wait for the processes to exit.281282If any of the processes already exited, Signal returns silently.283*/284func Signal(signal os.Signal) {285trackedSessionsMutex.Lock()286defer trackedSessionsMutex.Unlock()287for _, session := range trackedSessions {288session.Signal(signal)289}290}291292/*293Interrupt sends the SIGINT signal to all the processes started by Run.294It does not wait for the processes to exit.295296If any of the processes already exited, Interrupt returns silently.297*/298func Interrupt() {299trackedSessionsMutex.Lock()300defer trackedSessionsMutex.Unlock()301for _, session := range trackedSessions {302session.Interrupt()303}304}305306307