github.com/onsi/ginkgo@v1.16.6-0.20211118180735-4e1925ba4c95/internal/interrupt_handler/interrupt_handler.go (about)

     1  package interrupt_handler
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/signal"
     7  	"runtime"
     8  	"sync"
     9  	"syscall"
    10  	"time"
    11  
    12  	"github.com/onsi/ginkgo/formatter"
    13  	"github.com/onsi/ginkgo/internal/parallel_support"
    14  )
    15  
    16  const TIMEOUT_REPEAT_INTERRUPT_MAXIMUM_DURATION = 30 * time.Second
    17  const TIMEOUT_REPEAT_INTERRUPT_FRACTION_OF_TIMEOUT = 10
    18  const ABORT_POLLING_INTERVAL = 500 * time.Millisecond
    19  const ABORT_REPEAT_INTERRUPT_DURATION = 30 * time.Second
    20  
    21  type InterruptCause uint
    22  
    23  const (
    24  	InterruptCauseInvalid InterruptCause = iota
    25  
    26  	InterruptCauseSignal
    27  	InterruptCauseTimeout
    28  	InterruptCauseAbortByOtherProcess
    29  )
    30  
    31  func (ic InterruptCause) String() string {
    32  	switch ic {
    33  	case InterruptCauseSignal:
    34  		return "Interrupted by User"
    35  	case InterruptCauseTimeout:
    36  		return "Interrupted by Timeout"
    37  	case InterruptCauseAbortByOtherProcess:
    38  		return "Interrupted by Other Ginkgo Process"
    39  	}
    40  	return "INVALID_INTERRUPT_CAUSE"
    41  }
    42  
    43  type InterruptStatus struct {
    44  	Interrupted bool
    45  	Channel     chan interface{}
    46  	Cause       InterruptCause
    47  }
    48  
    49  type InterruptHandlerInterface interface {
    50  	Status() InterruptStatus
    51  	SetInterruptPlaceholderMessage(string)
    52  	ClearInterruptPlaceholderMessage()
    53  	InterruptMessageWithStackTraces() string
    54  }
    55  
    56  type InterruptHandler struct {
    57  	c                           chan interface{}
    58  	lock                        *sync.Mutex
    59  	interrupted                 bool
    60  	interruptPlaceholderMessage string
    61  	interruptCause              InterruptCause
    62  	client                      parallel_support.Client
    63  	stop                        chan interface{}
    64  }
    65  
    66  func NewInterruptHandler(timeout time.Duration, client parallel_support.Client) *InterruptHandler {
    67  	handler := &InterruptHandler{
    68  		c:           make(chan interface{}),
    69  		lock:        &sync.Mutex{},
    70  		interrupted: false,
    71  		stop:        make(chan interface{}),
    72  		client:      client,
    73  	}
    74  	handler.registerForInterrupts(timeout)
    75  	return handler
    76  }
    77  
    78  func (handler *InterruptHandler) Stop() {
    79  	close(handler.stop)
    80  }
    81  
    82  func (handler *InterruptHandler) registerForInterrupts(timeout time.Duration) {
    83  	// os signal handling
    84  	signalChannel := make(chan os.Signal, 1)
    85  	signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
    86  
    87  	// timeout handling
    88  	var timeoutChannel <-chan time.Time
    89  	var timeoutTimer *time.Timer
    90  	if timeout > 0 {
    91  		timeoutTimer = time.NewTimer(timeout)
    92  		timeoutChannel = timeoutTimer.C
    93  	}
    94  
    95  	// cross-process abort handling
    96  	var abortChannel chan bool
    97  	if handler.client != nil {
    98  		abortChannel = make(chan bool)
    99  		go func() {
   100  			pollTicker := time.NewTicker(ABORT_POLLING_INTERVAL)
   101  			for {
   102  				select {
   103  				case <-pollTicker.C:
   104  					if handler.client.ShouldAbort() {
   105  						abortChannel <- true
   106  						pollTicker.Stop()
   107  						return
   108  					}
   109  				case <-handler.stop:
   110  					pollTicker.Stop()
   111  					return
   112  				}
   113  			}
   114  		}()
   115  	}
   116  
   117  	// listen for any interrupt signals
   118  	// note that some (timeouts, cross-process aborts) will only trigger once
   119  	// for these we set up a ticker to keep interrupting the suite until it ends
   120  	// this ensures any `AfterEach` or `AfterSuite`s that get stuck cleaning up
   121  	// get interrupted eventually
   122  	go func() {
   123  		var interruptCause InterruptCause
   124  		var repeatChannel <-chan time.Time
   125  		var repeatTicker *time.Ticker
   126  		for {
   127  			select {
   128  			case <-signalChannel:
   129  				interruptCause = InterruptCauseSignal
   130  			case <-timeoutChannel:
   131  				interruptCause = InterruptCauseTimeout
   132  				repeatInterruptTimeout := timeout / time.Duration(TIMEOUT_REPEAT_INTERRUPT_FRACTION_OF_TIMEOUT)
   133  				if repeatInterruptTimeout > TIMEOUT_REPEAT_INTERRUPT_MAXIMUM_DURATION {
   134  					repeatInterruptTimeout = TIMEOUT_REPEAT_INTERRUPT_MAXIMUM_DURATION
   135  				}
   136  				timeoutTimer.Stop()
   137  				repeatTicker = time.NewTicker(repeatInterruptTimeout)
   138  				repeatChannel = repeatTicker.C
   139  			case <-abortChannel:
   140  				interruptCause = InterruptCauseAbortByOtherProcess
   141  				repeatTicker = time.NewTicker(ABORT_REPEAT_INTERRUPT_DURATION)
   142  				repeatChannel = repeatTicker.C
   143  			case <-repeatChannel:
   144  				//do nothing, just interrupt again using the same interruptCause
   145  			case <-handler.stop:
   146  				if timeoutTimer != nil {
   147  					timeoutTimer.Stop()
   148  				}
   149  				if repeatTicker != nil {
   150  					repeatTicker.Stop()
   151  				}
   152  				signal.Stop(signalChannel)
   153  				return
   154  			}
   155  			handler.lock.Lock()
   156  			handler.interruptCause = interruptCause
   157  			if handler.interruptPlaceholderMessage != "" {
   158  				fmt.Println(handler.interruptPlaceholderMessage)
   159  			}
   160  			handler.interrupted = true
   161  			close(handler.c)
   162  			handler.c = make(chan interface{})
   163  			handler.lock.Unlock()
   164  		}
   165  	}()
   166  }
   167  
   168  func (handler *InterruptHandler) Status() InterruptStatus {
   169  	handler.lock.Lock()
   170  	defer handler.lock.Unlock()
   171  
   172  	return InterruptStatus{
   173  		Interrupted: handler.interrupted,
   174  		Channel:     handler.c,
   175  		Cause:       handler.interruptCause,
   176  	}
   177  }
   178  
   179  func (handler *InterruptHandler) SetInterruptPlaceholderMessage(message string) {
   180  	handler.lock.Lock()
   181  	defer handler.lock.Unlock()
   182  
   183  	handler.interruptPlaceholderMessage = message
   184  }
   185  
   186  func (handler *InterruptHandler) ClearInterruptPlaceholderMessage() {
   187  	handler.lock.Lock()
   188  	defer handler.lock.Unlock()
   189  
   190  	handler.interruptPlaceholderMessage = ""
   191  }
   192  
   193  func (handler *InterruptHandler) InterruptMessageWithStackTraces() string {
   194  	handler.lock.Lock()
   195  	out := fmt.Sprintf("%s\n\n", handler.interruptCause.String())
   196  	defer handler.lock.Unlock()
   197  	if handler.interruptCause == InterruptCauseAbortByOtherProcess {
   198  		return out
   199  	}
   200  	out += "Here's a stack trace of all running goroutines:\n"
   201  	buf := make([]byte, 8192)
   202  	for {
   203  		n := runtime.Stack(buf, true)
   204  		if n < len(buf) {
   205  			buf = buf[:n]
   206  			break
   207  		}
   208  		buf = make([]byte, 2*len(buf))
   209  	}
   210  	out += formatter.Fi(1, "%s", string(buf))
   211  	return out
   212  }