github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/agent/transport/stream.go (about)

     1  package transport
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os/exec"
     7  	"runtime"
     8  	"sync"
     9  	"syscall"
    10  	"time"
    11  )
    12  
    13  // Stream implements io.ReadWriteCloser using the standard input and output
    14  // streams of an agent process, with closure implemented via termination
    15  // signaling heuristics designed to shut down agent processes reliably. It
    16  // guarantees that its Close method unblocks pending Read and Write calls. It
    17  // also provides optional forwarding of the process' standard error stream.
    18  type Stream struct {
    19  	// process is the underlying process.
    20  	process *exec.Cmd
    21  	// standardInput is the process' standard input stream.
    22  	standardInput io.WriteCloser
    23  	// standardOutput is the process' standard output stream.
    24  	standardOutput io.Reader
    25  	// terminationDelayLock restricts access to terminationDelay.
    26  	terminationDelayLock sync.Mutex
    27  	// terminationDelay specifies the duration that the stream should wait for
    28  	// the underlying process to exit on its own before performing termination.
    29  	terminationDelay time.Duration
    30  }
    31  
    32  // NewStream creates a new Stream instance that wraps the specified command
    33  // object. It must be called before the corresponding process is started and no
    34  // other I/O redirection should be performed for the process. If this function
    35  // fails, the command should be considered unusable. If standardErrorReceiver is
    36  // non-nil, then the process' standard error output will be forwarded to it.
    37  func NewStream(process *exec.Cmd, standardErrorReceiver io.Writer) (*Stream, error) {
    38  	// Create a pipe to the process' standard input stream.
    39  	standardInput, err := process.StdinPipe()
    40  	if err != nil {
    41  		return nil, fmt.Errorf("unable to redirect process input: %w", err)
    42  	}
    43  
    44  	// Create a pipe from the process' standard output stream.
    45  	standardOutput, err := process.StdoutPipe()
    46  	if err != nil {
    47  		standardInput.Close()
    48  		return nil, fmt.Errorf("unable to redirect process output: %w", err)
    49  	}
    50  
    51  	// If a standard error receiver has been specified, then create a pipe from
    52  	// the process' standard error stream and forward it to the receiver. We do
    53  	// this manually (instead of just assigning the receiver to process.Stderr)
    54  	// to avoid golang/go#23019. We perform the same closure on the standard
    55  	// error stream as os/exec's standard forwarding Goroutines, a fix designed
    56  	// to avoid golang/go#10400.
    57  	if standardErrorReceiver != nil {
    58  		standardError, err := process.StderrPipe()
    59  		if err != nil {
    60  			standardInput.Close()
    61  			standardOutput.Close()
    62  			return nil, fmt.Errorf("unable to redirect process error output: %w", err)
    63  		}
    64  		go func() {
    65  			io.Copy(standardErrorReceiver, standardError)
    66  			standardError.Close()
    67  		}()
    68  	}
    69  
    70  	// Create the result.
    71  	return &Stream{
    72  		process:        process,
    73  		standardInput:  standardInput,
    74  		standardOutput: standardOutput,
    75  	}, nil
    76  }
    77  
    78  // Read implements io.Reader.Read.
    79  func (s *Stream) Read(buffer []byte) (int, error) {
    80  	return s.standardOutput.Read(buffer)
    81  }
    82  
    83  // Write implements io.Writer.Write.
    84  func (s *Stream) Write(buffer []byte) (int, error) {
    85  	return s.standardInput.Write(buffer)
    86  }
    87  
    88  // SetTerminationDelay sets the termination delay for the stream. This method
    89  // will panic if terminationDelay is negative. This method is safe to call
    90  // concurrently with Close, though, if called concurrently, there is no
    91  // guarantee that the new delay will be set in time for Close to use it.
    92  func (s *Stream) SetTerminationDelay(terminationDelay time.Duration) {
    93  	// Validate the kill delay time.
    94  	if terminationDelay < 0 {
    95  		panic("negative termination delay specified")
    96  	}
    97  
    98  	// Lock and defer release of the termination delay lock.
    99  	s.terminationDelayLock.Lock()
   100  	defer s.terminationDelayLock.Unlock()
   101  
   102  	// Set the termination delay.
   103  	s.terminationDelay = terminationDelay
   104  }
   105  
   106  // Close closes the process' streams and terminates the process using heuristics
   107  // designed for agent transport processes. These heuristics are necessary to
   108  // avoid the problem described in golang/go#23019 and experienced in
   109  // mutagen-io/mutagen#223 and mutagen-io/mutagen-compose#11.
   110  //
   111  // First, if a non-negative, non-zero termination delay has been specified, then
   112  // this method will wait (up to the specified duration) for the process to exit
   113  // on its own. If the process exits on its own, then its standard input, output,
   114  // and error streams are closed and this method returns.
   115  //
   116  // Second, the process' standard input stream will be closed. The process will
   117  // then be given up to one second to exit on its own. If it does, then the
   118  // standard output and error streams are closed and this method returns. Closure
   119  // of the standard input stream is recognized by the Mutagen agent as indicating
   120  // termination and should thus be sufficient to cause termination for transport
   121  // processes that forward this closure correctly.
   122  //
   123  // Third, on POSIX systems only, the process will be sent a SIGTERM signal. The
   124  // process will then be given up to one second to exit on its own. If it does,
   125  // then the standard output and error streams are closed and this method
   126  // returns. Reception of SIGTERM is also recognized by the Mutagen agent as
   127  // indicating termination and should thus be sufficient to cause termination for
   128  // transport processes that correctly forward this signal. Windows lacks a
   129  // directly equivalent termination mechanism (the closest analog would be
   130  // sending WM_CLOSE, but reception and processing of such a message may have
   131  // unpredictable effects in different runtimes).
   132  //
   133  // Finally, the process will be sent a SIGKILL signal (on POSIX) or terminated
   134  // via TerminateProcess (on Windows). This method will then wait for the process
   135  // to exit before closing the standard output and error streams and returning.
   136  //
   137  // This method guarantees that, by the time it returns, the underlying transport
   138  // process has terminated and its associated standard input, output, and error
   139  // stream handles in the current process have been closed. The error returned by
   140  // this function will be that returned by os/exec.Cmd.Wait. Note, however, that
   141  // this method cannot guarantee that any or all child processes spawned by the
   142  // transport process have terminated by the time this method returns. This is
   143  // mostly due to operating system API limitations. Specifically, POSIX provides
   144  // no away to restrict subprocesses to a single process group and therefore
   145  // cannot guarantee that a call to killpg will reach all the subprocesses that
   146  // have been spawned. Even if that were possible, there is no mechanism to wait
   147  // for an entire process group to exit, and it's not well-defined exactly what
   148  // signals or stream closures should be used to signal those processes anyway,
   149  // because Mutagen is not privy to the internals of the transport process(es).
   150  // Windows, while it does provide a "job" API for managing and terminating
   151  // process hierarchies, is even less nuanced in its process signaling mechanism
   152  // (essentially offering only the equivalent of SIGKILL) and it's thus even less
   153  // clear how to signal termination there with arbitrary and opaque process
   154  // hierarchies. We thus rely on a certain level of well-behavedness when it
   155  // comes to transport processes. Specifically, we assume that they know how to
   156  // correctly handle and forward standard input closure and SIGTERM signals, and
   157  // that they'll terminate when their underlying agent process terminates.
   158  func (s *Stream) Close() error {
   159  	// Start a background Goroutine that will wait for the process to exit and
   160  	// return the wait result. We'll rely on this call to Wait to close the
   161  	// standard output and error streams. We don't have to worry about
   162  	// golang/go#23019 in this case because we're only using pipes and thus Wait
   163  	// doesn't have any internal copying Goroutines to wait on.
   164  	waitResults := make(chan error, 1)
   165  	go func() {
   166  		waitResults <- s.process.Wait()
   167  	}()
   168  
   169  	// Start by waiting for the process to terminate on its own.
   170  	s.terminationDelayLock.Lock()
   171  	terminationDelay := s.terminationDelay
   172  	s.terminationDelayLock.Unlock()
   173  	waitTimer := time.NewTimer(terminationDelay)
   174  	select {
   175  	case err := <-waitResults:
   176  		waitTimer.Stop()
   177  		return err
   178  	case <-waitTimer.C:
   179  	}
   180  
   181  	// Close the process' standard input and wait up to one second for it to
   182  	// terminate on its own.
   183  	s.standardInput.Close()
   184  	waitTimer.Reset(time.Second)
   185  	select {
   186  	case err := <-waitResults:
   187  		waitTimer.Stop()
   188  		return err
   189  	case <-waitTimer.C:
   190  	}
   191  
   192  	// If this is a POSIX system, then send SIGTERM to the process and wait up
   193  	// to one second for it to terminate on its own.
   194  	if runtime.GOOS != "windows" {
   195  		s.process.Process.Signal(syscall.SIGTERM)
   196  		waitTimer.Reset(time.Second)
   197  		select {
   198  		case err := <-waitResults:
   199  			waitTimer.Stop()
   200  			return err
   201  		case <-waitTimer.C:
   202  		}
   203  	}
   204  
   205  	// Kill the process (via SIGKILL on POSIX and TerminateProcess on Windows)
   206  	// and wait for it to exit.
   207  	s.process.Process.Kill()
   208  	return <-waitResults
   209  }