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 }