├── README.md ├── unix_listener.go ├── errors.go ├── tcp_listener.go └── spoon.go /README.md: -------------------------------------------------------------------------------- 1 | Package spoon 2 | ============= 3 | ![Project status](https://img.shields.io/badge/version-0.8.0-yellowgreen.svg) 4 | 5 | Package spoon is used to create zero-downtime upgradable programs and includes logic to gracefully handle TCP connections, including HTTP. 6 | 7 | NOTE: I've been using this in a production app for a few months without any problems whatsoever, so it should be good and stable. 8 | 9 | Contrived Example - using HTTP 10 | ----------------- 11 | 12 | ```go 13 | 14 | package main 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | 20 | "github.com/go-playground/lars" 21 | "github.com/go-playground/lars/examples/middleware/logging-recovery" 22 | "github.com/go-playground/spoon" 23 | ) 24 | 25 | func main() { 26 | 27 | // setup TCP connections 28 | sp := spoon.New() 29 | 30 | ltcp, err := sp.ListenTCP(":4444") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // let spoon know your; done setting up connections... if any 36 | err = sp.ListenerSetupComplete() 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | l := lars.New() 42 | l.Use(middleware.LoggingAndRecovery) 43 | 44 | l.Get("/", GetHome) 45 | 46 | // OPTIONAL: use TCP connection for HTTP, could have just used raw connection for non HTTP logic. 47 | err = ltcp.ListenAndServe(":4444", l.Serve()) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | // Run, will block here 53 | sp.Run() 54 | } 55 | 56 | // GetHome ... 57 | func GetHome(l lars.Context) { 58 | 59 | if err := l.Text(http.StatusOK, "Why spoon? because it doesn't fork!"); err != nil { 60 | log.Panic(err) 61 | } 62 | } 63 | 64 | 65 | ``` -------------------------------------------------------------------------------- /unix_listener.go: -------------------------------------------------------------------------------- 1 | package spoon 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func newUnixListener(l net.UnixListener) *unixListener { 12 | return &unixListener{ 13 | UnixListener: l, 14 | forceTimeoutDuration: time.Minute * 5, 15 | } 16 | } 17 | 18 | type unixListener struct { 19 | net.UnixListener 20 | forceTimeoutDuration time.Duration 21 | wg sync.WaitGroup 22 | } 23 | 24 | var _ net.Listener = new(unixListener) 25 | 26 | func (l *unixListener) Accept() (net.Conn, error) { 27 | 28 | conn, err := l.UnixListener.AcceptUnix() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | zconn := zeroUinxConn{ 34 | Conn: conn, 35 | wg: l.wg, 36 | } 37 | 38 | l.wg.Add(1) 39 | 40 | return zconn, nil 41 | } 42 | 43 | // blocking wait for close 44 | func (l *unixListener) Close() error { 45 | 46 | log.Println("Closing Listener:", l.Addr()) 47 | 48 | //stop accepting connections - release fd 49 | err := l.UnixListener.Close() 50 | c := make(chan struct{}) 51 | 52 | go func() { 53 | l.wg.Wait() 54 | close(c) 55 | }() 56 | 57 | select { 58 | case <-c: 59 | // closed gracefully 60 | case <-time.After(l.forceTimeoutDuration): 61 | l.SetDeadline(time.Now()) 62 | log.Println("timeout reached, force shutdown") 63 | // not waiting any longer, letting this go. 64 | // spoon will think it's been closed and when 65 | // the process dies, connections will get cut 66 | } 67 | 68 | return err 69 | } 70 | 71 | func (l *unixListener) File() *os.File { 72 | 73 | // returns a dup(2) - FD_CLOEXEC flag *not* set 74 | tl := l.UnixListener 75 | fl, _ := tl.File() 76 | 77 | return fl 78 | } 79 | 80 | //notifying on close net.Conn 81 | type zeroUinxConn struct { 82 | net.Conn 83 | wg sync.WaitGroup 84 | } 85 | 86 | func (conn zeroUinxConn) Close() (err error) { 87 | 88 | if err = conn.Conn.Close(); err != nil { 89 | return 90 | } 91 | 92 | conn.wg.Done() 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package spoon 2 | 3 | import "fmt" 4 | 5 | // FileDescriptorError contains a file descriptor error 6 | type FileDescriptorError struct { 7 | innerError error 8 | } 9 | 10 | // Error returns the child's start error 11 | func (f *FileDescriptorError) Error() string { 12 | return fmt.Sprint("FileDescriptor Error:" + f.innerError.Error()) 13 | } 14 | 15 | var _ error = new(FileDescriptorError) 16 | 17 | // ChildStartError contains the child's start error 18 | type ChildStartError struct { 19 | innerError error 20 | } 21 | 22 | // Error returns the child's start error 23 | func (s *ChildStartError) Error() string { 24 | return fmt.Sprint("Child Process Failed to Start:" + s.innerError.Error()) 25 | } 26 | 27 | var _ error = new(ChildStartError) 28 | 29 | // ChildShutdownError contains the child's shutdown error 30 | type ChildShutdownError struct { 31 | innerError error 32 | } 33 | 34 | // Error returns the child's start error 35 | func (s *ChildShutdownError) Error() string { 36 | return fmt.Sprint("Child Shutdown Error:" + s.innerError.Error() + "\n\nNOTE: could just be the force termination because timeout reached") 37 | } 38 | 39 | var _ error = new(ChildShutdownError) 40 | 41 | // SignalParentError contains an error regarding signaling the parent 42 | type SignalParentError struct { 43 | innerError error 44 | } 45 | 46 | // Error returns the child's start error 47 | func (s *SignalParentError) Error() string { 48 | return fmt.Sprint("Error Signaling parent:" + s.innerError.Error()) 49 | } 50 | 51 | var _ error = new(SignalParentError) 52 | 53 | // ChildCrashError contains the child's shutdown error 54 | type ChildCrashError struct { 55 | innerError error 56 | } 57 | 58 | // Error returns the child's start error 59 | func (s *ChildCrashError) Error() string { 60 | return fmt.Sprint("Child Crashed:" + s.innerError.Error()) 61 | } 62 | 63 | var _ error = new(ChildCrashError) 64 | 65 | // BinaryUpdateError contains a binary update error 66 | type BinaryUpdateError struct { 67 | innerError error 68 | } 69 | 70 | // Error returns the slave start error 71 | func (b *BinaryUpdateError) Error() string { 72 | return fmt.Sprint("Binary Update Error:" + b.innerError.Error()) 73 | } 74 | 75 | var _ error = new(BinaryUpdateError) 76 | -------------------------------------------------------------------------------- /tcp_listener.go: -------------------------------------------------------------------------------- 1 | package spoon 2 | 3 | import ( 4 | "crypto/tls" 5 | "log" 6 | "net" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const ( 14 | http2NextProtoTLS = "h2" 15 | http2Rev14 = "h2-14" 16 | http11 = "http/1.1" 17 | ) 18 | 19 | // TCPListener is spoon's Listener interface with extra helper methods. 20 | type TCPListener interface { 21 | net.Listener 22 | SetKeepAlive(d time.Duration) 23 | SetForceTimeout(d time.Duration) 24 | ListenAndServe(addr string, handler http.Handler) error 25 | ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error 26 | RunServer(server *http.Server) error 27 | } 28 | 29 | func newtcpListener(l *net.TCPListener) *tcpListener { 30 | return &tcpListener{ 31 | TCPListener: l, 32 | keepaliveDuration: 3 * time.Minute, 33 | forceTimeoutDuration: 5 * time.Minute, 34 | wg: new(sync.WaitGroup), 35 | conns: map[*net.TCPConn]*net.TCPConn{}, 36 | m: new(sync.Mutex), 37 | } 38 | } 39 | 40 | type tcpListener struct { 41 | *net.TCPListener 42 | keepaliveDuration time.Duration 43 | forceTimeoutDuration time.Duration 44 | wg *sync.WaitGroup 45 | conns map[*net.TCPConn]*net.TCPConn 46 | m *sync.Mutex 47 | } 48 | 49 | var _ net.Listener = new(tcpListener) 50 | var _ TCPListener = new(tcpListener) 51 | 52 | func (l *tcpListener) Accept() (net.Conn, error) { 53 | 54 | conn, err := l.TCPListener.AcceptTCP() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | conn.SetKeepAlive(true) // see http.tcpKeepAliveListener 60 | conn.SetKeepAlivePeriod(l.keepaliveDuration) // see http.tcpKeepAliveListener 61 | // conn.SetLinger(0) // is the default already accoring to the docs https://golang.org/pkg/net/#TCPConn.SetLinger 62 | 63 | zconn := zeroTCPConn{ 64 | TCPConn: conn, 65 | wg: l.wg, 66 | l: l, 67 | } 68 | 69 | l.m.Lock() 70 | l.conns[conn] = conn 71 | l.m.Unlock() 72 | 73 | l.wg.Add(1) 74 | 75 | return zconn, nil 76 | } 77 | 78 | // blocking wait for close 79 | func (l *tcpListener) Close() error { 80 | 81 | //stop accepting connections - release fd 82 | err := l.TCPListener.Close() 83 | c := make(chan struct{}) 84 | 85 | go func() { 86 | l.m.Lock() 87 | for _, v := range l.conns { 88 | v.Close() // this is OK to close, see (*TCPConn) SetLinger, just can't reduce waitgroup until it's actually closed! 89 | } 90 | l.m.Unlock() 91 | }() 92 | 93 | go func() { 94 | l.wg.Wait() 95 | close(c) 96 | }() 97 | 98 | select { 99 | case <-c: 100 | // closed gracefully 101 | case <-time.After(l.forceTimeoutDuration): 102 | log.Println("timeout reached, force shutdown") 103 | // not waiting any longer, letting this go. 104 | // spoon will think it's been closed and when 105 | // the process dies, connections will get cut 106 | } 107 | 108 | return err 109 | } 110 | 111 | func (l *tcpListener) File() *os.File { 112 | 113 | // returns a dup(2) - FD_CLOEXEC flag *not* set 114 | tl := l.TCPListener 115 | fl, _ := tl.File() 116 | 117 | return fl 118 | } 119 | 120 | //notifying on close net.Conn 121 | type zeroTCPConn struct { 122 | *net.TCPConn 123 | wg *sync.WaitGroup 124 | l *tcpListener 125 | } 126 | 127 | func (conn zeroTCPConn) Close() (err error) { 128 | 129 | if err = conn.TCPConn.Close(); err != nil { 130 | log.Println("ERROR CLOSING CONNECTION, OK if connection already closed, we must have triggered a restart: ", err) 131 | } 132 | 133 | conn.l.m.Lock() 134 | delete(conn.l.conns, conn.TCPConn) 135 | conn.l.m.Unlock() 136 | 137 | conn.wg.Done() 138 | return 139 | } 140 | 141 | // 142 | // HTTP Section for tcpListener 143 | // 144 | 145 | // ListenAndServe mimics the std libraries http.ListenAndServe but uses our custom listener 146 | // for graceful restarts. 147 | // NOTE: addr is ignored, the address of the listener is used, only reason it is a param is for 148 | // easier conversion from stdlib http.ListenAndServe 149 | func (l *tcpListener) ListenAndServe(addr string, handler http.Handler) error { 150 | 151 | server := &http.Server{Addr: l.Addr().String(), Handler: handler} 152 | 153 | go server.Serve(l) 154 | 155 | return nil 156 | } 157 | 158 | // ListenAndServeTLS mimics the std libraries http.ListenAndServeTLS but uses out custom listener 159 | // for graceful restarts. 160 | // NOTE: addr is ignored, the address of the listener is used, only reason it is a param is for 161 | // easier conversion from stdlib http.ListenAndServeTLS 162 | func (l *tcpListener) ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error { 163 | 164 | var err error 165 | 166 | tlsConfig := &tls.Config{ 167 | NextProtos: []string{http2NextProtoTLS, http2Rev14, http11}, 168 | Certificates: make([]tls.Certificate, 1), 169 | } 170 | 171 | tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | tlsListener := tls.NewListener(l, tlsConfig) 177 | 178 | server := &http.Server{Addr: tlsListener.Addr().String(), Handler: handler, TLSConfig: tlsConfig} 179 | 180 | go server.Serve(tlsListener) 181 | 182 | return nil 183 | } 184 | 185 | // RunServer runs the provided http.Server, if using TLS, TLSConfig must be setup prior to 186 | // calling this function 187 | func (l *tcpListener) RunServer(server *http.Server) error { 188 | 189 | var lis net.Listener 190 | 191 | if server.TLSConfig != nil { 192 | 193 | if len(server.TLSConfig.NextProtos) == 0 { 194 | server.TLSConfig.NextProtos = append(server.TLSConfig.NextProtos, http2NextProtoTLS, http2Rev14, http11) 195 | } 196 | 197 | lis = tls.NewListener(l, server.TLSConfig) 198 | } else { 199 | lis = l 200 | } 201 | 202 | go server.Serve(lis) 203 | 204 | return nil 205 | } 206 | 207 | // SetKeepAlive sets the listener's connection keep alive timeout. 208 | // NOTE: method is NOT thread safe, must set prior to sp.Run() 209 | // DEFAULT: time.Minute * 3 210 | func (l *tcpListener) SetKeepAlive(d time.Duration) { 211 | l.keepaliveDuration = d 212 | } 213 | 214 | // SetKeepAlive sets the listener's connection keep alive timeout. 215 | // NOTE: method is NOT thread safe, must set prior to sp.Run() 216 | // DEFAULT: time.Minute * 5 217 | func (l *tcpListener) SetForceTimeout(d time.Duration) { 218 | l.forceTimeoutDuration = d 219 | } 220 | 221 | func strSliceContains(ss []string, s string) bool { 222 | for _, v := range ss { 223 | if v == s { 224 | return true 225 | } 226 | } 227 | return false 228 | } 229 | -------------------------------------------------------------------------------- /spoon.go: -------------------------------------------------------------------------------- 1 | package spoon 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | "os/exec" 12 | "os/signal" 13 | "strconv" 14 | "sync" 15 | "sync/atomic" 16 | "syscall" 17 | "time" 18 | 19 | "github.com/kardianos/osext" 20 | ) 21 | 22 | const ( 23 | envListenerFDS = "GO_LISTENER_FDS" 24 | envActive = "GO_ACTIVE_PROCESSES" 25 | ) 26 | 27 | // Spoon contains one or more connection information 28 | // for graceful restarts 29 | type Spoon struct { 30 | binaryPath string 31 | binaryNewPath string 32 | binaryOldPath string 33 | fileDescriptors []*os.File 34 | fileDescriptorIndex int 35 | listeners []net.Listener 36 | activeProcesses *int32 37 | child *exec.Cmd 38 | errorChan chan error 39 | restartChan chan struct{} 40 | binaryChecksum string 41 | m sync.RWMutex 42 | } 43 | 44 | // New creates a new spoon instance. 45 | func New() *Spoon { 46 | 47 | executable, err := osext.Executable() 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | var active int32 53 | 54 | sp := &Spoon{ 55 | binaryPath: executable, 56 | binaryNewPath: executable + ".new", 57 | binaryOldPath: executable + ".old", 58 | fileDescriptorIndex: 3, // they start at 3 59 | activeProcesses: &active, 60 | errorChan: make(chan error), 61 | restartChan: make(chan struct{}), 62 | } 63 | 64 | if err = sp.ensureChecksum(); err != nil { 65 | panic(err) 66 | } 67 | 68 | return sp 69 | } 70 | 71 | func (s *Spoon) startChild() error { 72 | 73 | fds := envListenerFDS + "=" + strconv.Itoa(len(s.fileDescriptors)) 74 | ap := envActive + "=" + strconv.Itoa(int(atomic.LoadInt32(s.activeProcesses))) 75 | e := append(os.Environ(), fds, ap) 76 | 77 | oldCmd := s.child 78 | 79 | // start server 80 | s.child = exec.Command(s.binaryPath) 81 | s.child.Env = e 82 | s.child.Args = os.Args 83 | s.child.Stdin = os.Stdin 84 | s.child.Stdout = os.Stdout 85 | s.child.Stderr = os.Stderr 86 | s.child.ExtraFiles = s.fileDescriptors 87 | 88 | // wait for close signals here 89 | signals := make(chan os.Signal) 90 | signal.Notify(signals, syscall.SIGUSR1) 91 | 92 | go func() { 93 | 94 | // wait for child to signal it is up and running 95 | <-signals // child notifies master process that it's up and running 96 | 97 | log.Println("SIGUSR1 Recieved from child") 98 | 99 | signal.Stop(signals) 100 | 101 | // keep track of active processes to ensure at least one is running 102 | atomic.AddInt32(s.activeProcesses, 1) 103 | 104 | // will have to use current s.child, if set, to trigger shutdown of old child 105 | if oldCmd != nil { 106 | // shut down old server! .. gracefully 107 | oldCmd.Process.Signal(syscall.SIGTERM) 108 | } 109 | 110 | go func() { 111 | 112 | err := s.child.Wait() 113 | if err != nil { 114 | s.sendError(&ChildShutdownError{innerError: err}) 115 | } 116 | 117 | log.Println("Child Shutdown Complete") 118 | 119 | atomic.AddInt32(s.activeProcesses, -1) 120 | 121 | // ensure that at least one instance is running 122 | // this is just in case the child process crashes 123 | // due to an unexpected error and the maaster process 124 | // is still running, but without any children! 125 | // 126 | // a little outside the scope of this library but better to be up 127 | // than not! 128 | 129 | if atomic.LoadInt32(s.activeProcesses) == 0 { 130 | 131 | // no children running!... start one back up! 132 | // and notify of error. 133 | 134 | s.sendError(&ChildCrashError{innerError: errors.New("Unexpected Child End of Process, attempting restart")}) 135 | 136 | err := s.startChild() 137 | if err != nil { 138 | s.sendError(&ChildStartError{innerError: err}) 139 | } 140 | 141 | } 142 | }() 143 | }() 144 | 145 | if err := s.child.Start(); err != nil { 146 | return &ChildStartError{innerError: err} 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // Checksum returns the current binary's checksum value 153 | // for use in update requests etc... 154 | func (s *Spoon) Checksum() (checksum string) { 155 | 156 | s.m.RLock() 157 | checksum = s.binaryChecksum 158 | s.m.RUnlock() 159 | 160 | return 161 | } 162 | 163 | // Restart triggers a service zero downtime restart 164 | // NOTE: it is up to you to ensure Upgrade and Restart don't occur 165 | // at the same time, otherwise I would be limiting how you could use it 166 | func (s *Spoon) Restart() { 167 | 168 | // if in slave signal master process 169 | // with syscall.SIGUSR2 which will cause 170 | // restart 171 | if s.isSlaveProcess() { 172 | go s.signalParent(syscall.SIGUSR2) 173 | return 174 | } 175 | 176 | s.restartChan <- struct{}{} 177 | } 178 | 179 | // ListenerSetupComplete when in the master process, starts the first child 180 | // otherwise if in the child process is just ignored. 181 | func (s *Spoon) ListenerSetupComplete() error { 182 | 183 | // not need to do anything 184 | if s.isSlaveProcess() { 185 | return nil 186 | } 187 | 188 | // in master process, start new child process 189 | // blocks until getting a termination signal. 190 | 191 | // starting goroutine to monitor restart signals from children 192 | go func() { 193 | 194 | signals := make(chan os.Signal) 195 | signal.Notify(signals, syscall.SIGUSR2) 196 | 197 | for { 198 | <-signals 199 | 200 | log.Println("Recieved Restart signal from Child") 201 | 202 | s.Restart() 203 | } 204 | }() 205 | 206 | if err := s.startChild(); err != nil { 207 | return err 208 | } 209 | 210 | go func() { 211 | for { 212 | <-s.restartChan 213 | 214 | log.Println("Graceful restart triggered") 215 | 216 | // graceful restart triggered 217 | err := s.startChild() 218 | if err != nil { 219 | s.sendError(&ChildStartError{innerError: fmt.Errorf("ERROR starting new slave gracefully %s", err)}) 220 | } 221 | } 222 | }() 223 | 224 | // wait for close signals here 225 | signals := make(chan os.Signal) 226 | signal.Notify(signals, syscall.SIGTERM) 227 | 228 | <-signals 229 | 230 | return nil 231 | } 232 | 233 | // Run when in the child process, notifies the master process that it has completed startup. 234 | // and will block until program is shutdown. 235 | func (s *Spoon) Run() { 236 | 237 | // should never reach this code from master process as 238 | // ListenerSetupComplete() should block. 239 | // but just in case 240 | if !s.isSlaveProcess() { 241 | panic("ERROR: Run should never be called from master process, please check that ListenerSetupComplete() was called.") 242 | } 243 | 244 | done := make(chan bool) 245 | signals := make(chan os.Signal) 246 | signal.Notify(signals, syscall.SIGTERM) 247 | 248 | go func() { 249 | <-signals 250 | 251 | log.Println("TERMINATION SIGNAL RECEIVED, Closing Slave Listener(s)") 252 | 253 | closed := make(chan int) 254 | var mt sync.Mutex 255 | var i int 256 | 257 | for _, l := range s.listeners { 258 | go func(l net.Listener) { 259 | 260 | log.Println("Closing Listener:", l.Addr()) 261 | 262 | err := l.Close() 263 | 264 | mt.Lock() 265 | i++ 266 | 267 | if err != nil { 268 | log.Println("There was an error shutting down the listener:", err, " continuing shutdown") 269 | } else { 270 | log.Printf("Gracefully shutdown server %d of %d\n", i, len(s.listeners)) 271 | } 272 | 273 | if i == len(s.listeners) { 274 | closed <- 0 275 | } 276 | 277 | mt.Unlock() 278 | }(l) 279 | } 280 | 281 | os.Exit(<-closed) 282 | }() 283 | 284 | // let's just wait a few seconds to ensure all listeners have completed startup 285 | // I don't know of a way to tell if they are already running or not 100%, the stdlib 286 | // has no way to hook into it that I know of. 287 | // 288 | // if 0 then it's first slave to be started, don't wait! 289 | if s.getExtraParams(envActive) != "0" { 290 | time.Sleep(time.Second * 3) 291 | } 292 | 293 | go s.signalParent(syscall.SIGUSR1) 294 | 295 | <-done 296 | } 297 | 298 | // Errors returns an error channel that can optionally be listened to 299 | // if you need to know if an error, or a specific error, has occurred. 300 | // eg. I send the dev team an email when something went wrong ( which should be never ) 301 | // but just in case. 302 | func (s *Spoon) Errors() <-chan error { 303 | 304 | if s.errorChan == nil { 305 | s.errorChan = make(chan error) 306 | } 307 | 308 | return s.errorChan 309 | } 310 | 311 | func (s *Spoon) sendError(err error) { 312 | 313 | if s.errorChan == nil { 314 | log.Println(err) 315 | } else { 316 | s.errorChan <- err 317 | } 318 | } 319 | 320 | func (s *Spoon) signalParent(sig os.Signal) { 321 | 322 | time.Sleep(time.Second) 323 | pid := os.Getppid() 324 | 325 | proc, err := os.FindProcess(pid) 326 | if err != nil { 327 | s.sendError(&SignalParentError{fmt.Errorf("ERROR FINDING MASTER PROCESS: %s", err)}) 328 | } 329 | 330 | err = proc.Signal(sig) 331 | if err != nil { 332 | s.sendError(&SignalParentError{fmt.Errorf("ERROR SIGNALING MASTER PROC: %s", err)}) 333 | } 334 | } 335 | 336 | // ListenTCP announces on the local network address laddr. The network net must 337 | // be: "tcp", "tcp4" or "tcp6". It returns an inherited net.Listener for the 338 | // matching network and address, or creates a new one using net.ListenTCP. 339 | func (s *Spoon) ListenTCP(addr string) (TCPListener, error) { 340 | 341 | if !s.isSlaveProcess() { 342 | 343 | // setup file descriptors 344 | 345 | a, err := net.ResolveTCPAddr("tcp", addr) 346 | if err != nil { 347 | return nil, &FileDescriptorError{innerError: fmt.Errorf("Invalid address %s (%s)", addr, err)} 348 | } 349 | 350 | l, err := net.ListenTCP("tcp", a) 351 | if err != nil { 352 | return nil, &FileDescriptorError{innerError: err} 353 | } 354 | 355 | f, err := l.File() 356 | if err != nil { 357 | return nil, &FileDescriptorError{innerError: fmt.Errorf("Failed to retreive fd for: %s (%s)", addr, err)} 358 | } 359 | 360 | if err := l.Close(); err != nil { 361 | return nil, &FileDescriptorError{innerError: fmt.Errorf("Failed to close listener for: %s (%s)", addr, err)} 362 | } 363 | 364 | s.fileDescriptors = append(s.fileDescriptors, f) 365 | 366 | return nil, nil 367 | } 368 | 369 | f := os.NewFile(uintptr(s.fileDescriptorIndex), "") 370 | s.fileDescriptorIndex++ 371 | 372 | l, err := net.FileListener(f) 373 | if err != nil { 374 | fmt.Println(err) 375 | return nil, &FileDescriptorError{innerError: fmt.Errorf("failed to inherit file descriptor: %d error: %s", s.fileDescriptorIndex, err)} 376 | } 377 | 378 | ltcp := newtcpListener(l.(*net.TCPListener)) 379 | s.listeners = append(s.listeners, ltcp) 380 | 381 | return ltcp, nil 382 | } 383 | 384 | func (s *Spoon) isSlaveProcess() bool { 385 | return s.getExtraParams(envListenerFDS) != "" 386 | } 387 | 388 | func (s *Spoon) getExtraParams(key string) string { 389 | return os.Getenv(key) 390 | } 391 | 392 | func (s *Spoon) ensureChecksum() error { 393 | s.m.Lock() 394 | defer s.m.Unlock() 395 | 396 | if s.binaryChecksum == "" { 397 | // set original binary checksum 398 | f, err := os.Open(s.binaryPath) 399 | if err != nil { 400 | return err 401 | } 402 | defer f.Close() 403 | 404 | hash := sha256.New() 405 | io.Copy(hash, f) 406 | 407 | s.binaryChecksum = fmt.Sprintf("%x", string(hash.Sum(nil))) 408 | } 409 | 410 | return nil 411 | } 412 | 413 | // UpgradeFullBinary updates the binary and must provide an sha256 414 | // checksum so that it can verify the binary read from the io.Reader 415 | // is complete. 416 | // NOTE: it is up to you to ensure Upgrade and Restart don't occur 417 | // at the same time, otherwise I would be limiting how you could use it 418 | func (s *Spoon) UpgradeFullBinary(r io.Reader, sha256Checksum string) error { 419 | 420 | hash := sha256.New() 421 | tee := io.TeeReader(r, hash) 422 | 423 | newFile, err := os.OpenFile(s.binaryNewPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0755)) 424 | if err != nil { 425 | return &BinaryUpdateError{innerError: fmt.Errorf("Failed to open new binary for writing: %v\n", err)} 426 | } 427 | defer newFile.Close() 428 | 429 | _, err = io.Copy(newFile, tee) 430 | if err != nil { 431 | return &BinaryUpdateError{innerError: fmt.Errorf("Failed writing to updated binary: %v\n", err)} 432 | } 433 | 434 | // must close the file handle otherwise some systems like windows 435 | // will see it as in-use(locked) and not allow us to move it to the 436 | // existing binary path 437 | newFile.Close() 438 | 439 | checksum := fmt.Sprintf("%x", string(hash.Sum(nil))) 440 | 441 | if checksum != sha256Checksum { 442 | os.Remove(s.binaryNewPath) 443 | return &BinaryUpdateError{innerError: fmt.Errorf("Checksums do not match: %v\n", err)} 444 | } 445 | 446 | // ensure any old files get cleaned up that were left hanging around 447 | os.Remove(s.binaryOldPath) 448 | 449 | // move existing binary to .old 450 | err = os.Rename(s.binaryPath, s.binaryOldPath) 451 | if err != nil { 452 | return &BinaryUpdateError{innerError: fmt.Errorf("Failed moving existing binary: %v\n", err)} 453 | } 454 | 455 | // move new binary into existing binary's old position 456 | err = os.Rename(s.binaryNewPath, s.binaryPath) 457 | if err != nil { 458 | // was an error moving new file into position 459 | // let's attempt to put back old binary 460 | errr := os.Rename(s.binaryOldPath, s.binaryPath) 461 | if errr != nil { 462 | return &BinaryUpdateError{innerError: fmt.Errorf("Failed moving new binary into place & Failed to move old binary back. App is now in a bad state: %v\n", err)} 463 | } 464 | 465 | return &BinaryUpdateError{innerError: fmt.Errorf("Failed moving new binary into place: %v\n", err)} 466 | } 467 | 468 | // this may fail on windows because old process has yet to shutdown, but that's 469 | // ok... it will get deleted on next upgrade. 470 | os.Remove(s.binaryOldPath) 471 | 472 | s.m.Lock() 473 | defer s.m.Unlock() 474 | 475 | // double checking the update was applied, in rare...rare cases it is possible 476 | // to think it's applied the update without error. 477 | if checksum == s.binaryChecksum { 478 | return &BinaryUpdateError{innerError: errors.New("Binary Not Updated! Even though no errors occured!")} 479 | } 480 | 481 | s.binaryChecksum = checksum 482 | 483 | return nil 484 | } 485 | --------------------------------------------------------------------------------