github.com/puellanivis/breton@v0.2.16/lib/os/process/context.go (about)

     1  package process
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"runtime/debug"
    10  	"sync"
    11  	"syscall"
    12  
    13  	"github.com/puellanivis/breton/lib/sync/edge"
    14  )
    15  
    16  // signaler provides a basic api for channels to support optional signal handling, like for SIGHUP.
    17  type signaler struct {
    18  	sync.Once
    19  	ch chan struct{}
    20  }
    21  
    22  // get returns a receive-only copy of the underlying channel for the signaler.
    23  // If the channel does not exist, it will be allocated.
    24  func (h *signaler) get() <-chan struct{} {
    25  	h.Do(func() {
    26  		h.ch = make(chan struct{})
    27  	})
    28  
    29  	return h.ch
    30  }
    31  
    32  // trySend does the job of trying to send the optional notification to the underlying channel.
    33  // It will return true if it successfully sends a notification.
    34  func (h *signaler) trySend() bool {
    35  	select {
    36  	case h.ch <- struct{}{}: // A send on a `nil` channel always blocks.
    37  		return true
    38  	default:
    39  	}
    40  
    41  	return false
    42  }
    43  
    44  var sigHandler struct {
    45  	sync.Once
    46  	ctx context.Context
    47  	ch  chan os.Signal
    48  	e   edge.Edge
    49  
    50  	hup signaler
    51  }
    52  
    53  // HangupChannel provides a receiver channel that will notify the caller of when a SIGHUP signal has been received.
    54  // This signal is often used by daemons to be notified of a request to reload configuration data.
    55  //
    56  // If the signal handler would block trzing to send this notification,
    57  // then it will treat the signal the same as any other terminating signal.
    58  func HangupChannel() <-chan struct{} {
    59  	return sigHandler.hup.get()
    60  }
    61  
    62  // signalHandler starts a long-lived goroutine, which will run in the background until the process ends.
    63  // It returns a `context.Context` that will be canceled in the event of a signal being received.
    64  // It will then continue to listen for more signals,
    65  // If it receives three signals, then we presume that we are not shutting down properly, and panic with all stacktraces.
    66  func signalHandler(parent context.Context) context.Context {
    67  	ctx, cancel := context.WithCancel(parent)
    68  
    69  	go func() {
    70  		killChan := make(chan struct{}, 3)
    71  
    72  		signal.Notify(sigHandler.ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
    73  		for sig := range sigHandler.ch {
    74  			fmt.Fprintln(os.Stderr, "received signal:", sig)
    75  
    76  			switch sig {
    77  			case syscall.SIGQUIT:
    78  				debug.SetTraceback("all")
    79  				panic("SIGQUIT")
    80  
    81  			case syscall.SIGHUP:
    82  				if sigHandler.hup.trySend() {
    83  					continue
    84  				}
    85  			}
    86  
    87  			// Prospectively transition the edge, so we can maybe avoid injecting an unnecessary SIGTERM.
    88  			sigHandler.e.Up()
    89  
    90  			cancel()
    91  
    92  			select {
    93  			case killChan <- struct{}{}: // killChan is not full, keep handling signals.
    94  			default:
    95  				// We have gotten three signals and we are still kicking,
    96  				// panic and dump all stack traces.
    97  				debug.SetTraceback("all")
    98  				panic("not responding to signals")
    99  			}
   100  		}
   101  
   102  		debug.SetTraceback("all")
   103  		panic("signal handler channel unexpectedly closed")
   104  	}()
   105  
   106  	return ctx
   107  }
   108  
   109  // Context returns a process-level `context.Context`
   110  // that is cancelled when the program receives a termination signal.
   111  //
   112  // A process should start a graceful shutdown process once this context is cancelled.
   113  //
   114  // This context should not be used as a parent to any requests,
   115  // otherwise those requests will also be cancelled
   116  // instead of being allowed to complete their work.
   117  func Context() context.Context {
   118  	sigHandler.Do(func() {
   119  		sigHandler.ch = make(chan os.Signal, 1)
   120  		sigHandler.ctx = signalHandler(context.Background())
   121  	})
   122  
   123  	return sigHandler.ctx
   124  }
   125  
   126  // Shutdown starts any graceful shutdown processes waiting for `process.Context()` to be cancelled.
   127  //
   128  // Shutdown works by injecting a `syscall.SIGTERM` directly to the signal handler,
   129  // which will cancel the `process.Context()` the same as a real SIGTERM.
   130  //
   131  // Shutdown returns an error only if it is unable to send the signal.
   132  // Notably, it is not an error to call Shutdown if we have already triggered a graceful shutdown.
   133  //
   134  // Shutdown does not wait for anything to finish before returning.
   135  func Shutdown() error {
   136  	if !sigHandler.e.Up() {
   137  		// We have already triggered a graceful shutdown, so nothing to do.
   138  		return nil
   139  	}
   140  
   141  	select {
   142  	case sigHandler.ch <- syscall.SIGTERM:
   143  		return nil
   144  	default:
   145  	}
   146  
   147  	return errors.New("could not send signal")
   148  }
   149  
   150  // Quit ends the program as soon as possible, dumping a stacktrace of all goroutines.
   151  //
   152  // Quit works by injecting a `syscall.SIGQUIT` directly to the signal handler,
   153  // which will cause a panic, and stacktrace of all goroutines the same as a real SIGQUIT.
   154  //
   155  // If Quit cannot inject the signal,
   156  // it will setup an unrecoverable panic to occur.
   157  //
   158  // In all cases, Quit will not return.
   159  func Quit() {
   160  	select {
   161  	case sigHandler.ch <- syscall.SIGQUIT:
   162  	default:
   163  		// We start up a separate goroutine for this to ensure that no `recover()` can block this panic.
   164  		go func() {
   165  			debug.SetTraceback("all")
   166  			panic("process was force quit")
   167  		}()
   168  	}
   169  
   170  	select {} // Block forever so we never return.
   171  }