├── .gitignore ├── LICENSE ├── README.md ├── example ├── double │ ├── .gitignore │ └── main.go ├── legacy │ ├── .gitignore │ └── main.go ├── sendto │ └── .gitignore └── single │ ├── .gitignore │ └── main.go ├── goagain.go ├── legacy.go └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.[68] 2 | *.a 3 | *.swp 4 | _obj 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Richard Crowley. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY RICHARD CROWLEY ``AS IS'' AND ANY EXPRESS 16 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL RICHARD CROWLEY OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation 28 | are those of the authors and should not be interpreted as representing 29 | official policies, either expressed or implied, of Richard Crowley. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goagain 2 | ======= 3 | 4 | Zero-downtime restarts in Go 5 | ---------------------------- 6 | 7 | The `goagain` package provides primitives for bringing zero-downtime restarts to Go applications that accept connections from a [`net.TCPListener`](http://golang.org/pkg/net/#TCPListener) or [`net.UnixListener`](http://golang.org/pkg/net/#UnixListener). 8 | 9 | Have a look at the examples because it isn't just a matter of importing the library and everything working. Your `main` function will have to accomodate the `goagain` protocols and your process will have to have some definition (however contrived you like) of a graceful shutdown process. 10 | 11 | Installation 12 | ------------ 13 | 14 | go get github.com/rcrowley/goagain 15 | 16 | Usage 17 | ----- 18 | 19 | Send `SIGUSR2` to a process using `goagain` and it will restart without downtime. 20 | 21 | [`example/single/main.go`](https://github.com/rcrowley/goagain/blob/master/example/single/main.go): The `Single` strategy (named because it calls `execve`(2) once) operates similarly to Nginx and Unicorn. The parent forks a child, the child execs, and then the child kills the parent. This is easy to understand but doesn't play nicely with Upstart and similar direct-supervision `init`(8) daemons. It should play nicely with `systemd`. 22 | 23 | [`example/double/main.go`](https://github.com/rcrowley/goagain/blob/master/example/double/main.go): The `Double` strategy (named because it calls `execve`(2) twice) is **experimental** so proceed with caution. The parent forks a child, the child execs, the child signals the parent, the parent execs, and finally the parent kills the child. This is regrettably much more complicated but plays nicely with Upstart and similar direct-supervision `init`(8) daemons. 24 | -------------------------------------------------------------------------------- /example/double/.gitignore: -------------------------------------------------------------------------------- 1 | double 2 | -------------------------------------------------------------------------------- /example/double/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/rcrowley/goagain" 5 | "fmt" 6 | "log" 7 | "net" 8 | "sync" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | func init() { 14 | goagain.Strategy = goagain.Double 15 | log.SetFlags(log.Lmicroseconds | log.Lshortfile) 16 | log.SetPrefix(fmt.Sprintf("pid:%d ", syscall.Getpid())) 17 | } 18 | 19 | func main() { 20 | 21 | // Inherit a net.Listener from our parent process or listen anew. 22 | ch := make(chan struct{}) 23 | wg := &sync.WaitGroup{} 24 | wg.Add(1) 25 | l, err := goagain.Listener() 26 | if nil != err { 27 | 28 | // Listen on a TCP or a UNIX domain socket (TCP here). 29 | l, err = net.Listen("tcp", "127.0.0.1:48879") 30 | if nil != err { 31 | log.Fatalln(err) 32 | } 33 | log.Println("listening on", l.Addr()) 34 | 35 | // Accept connections in a new goroutine. 36 | go serve(l, ch, wg) 37 | 38 | } else { 39 | 40 | // Resume listening and accepting connections in a new goroutine. 41 | log.Println("resuming listening on", l.Addr()) 42 | go serve(l, ch, wg) 43 | 44 | // If this is the child, send the parent SIGUSR2. If this is the 45 | // parent, send the child SIGQUIT. 46 | if err := goagain.Kill(); nil != err { 47 | log.Fatalln(err) 48 | } 49 | 50 | } 51 | 52 | // Block the main goroutine awaiting signals. 53 | sig, err := goagain.Wait(l) 54 | if nil != err { 55 | log.Fatalln(err) 56 | } 57 | 58 | // Do whatever's necessary to ensure a graceful exit like waiting for 59 | // goroutines to terminate or a channel to become closed. 60 | // 61 | // In this case, we'll close the channel to signal the goroutine to stop 62 | // accepting connections and wait for the goroutine to exit. 63 | close(ch) 64 | wg.Wait() 65 | 66 | // If we received SIGUSR2, re-exec the parent process. 67 | if goagain.SIGUSR2 == sig { 68 | if err := goagain.Exec(l); nil != err { 69 | log.Fatalln(err) 70 | } 71 | } 72 | 73 | } 74 | 75 | // A very rude server that says hello and then closes your connection. 76 | func serve(l net.Listener, ch chan struct{}, wg *sync.WaitGroup) { 77 | defer wg.Done() 78 | for { 79 | 80 | // Break out of the accept loop on the next iteration after the 81 | // process was signaled and our channel was closed. 82 | select { 83 | case <-ch: 84 | return 85 | default: 86 | } 87 | 88 | // Set a deadline so Accept doesn't block forever, which gives 89 | // us an opportunity to stop gracefully. 90 | l.(*net.TCPListener).SetDeadline(time.Now().Add(100e6)) 91 | 92 | c, err := l.Accept() 93 | if nil != err { 94 | if goagain.IsErrClosing(err) { 95 | return 96 | } 97 | if err.(*net.OpError).Timeout() { 98 | continue 99 | } 100 | log.Fatalln(err) 101 | } 102 | c.Write([]byte("Hello, world!\n")) 103 | c.Close() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /example/legacy/.gitignore: -------------------------------------------------------------------------------- 1 | legacy 2 | -------------------------------------------------------------------------------- /example/legacy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rcrowley/goagain" 6 | "log" 7 | "net" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func init() { 13 | log.SetFlags(log.Lmicroseconds | log.Lshortfile) 14 | log.SetPrefix(fmt.Sprintf("pid:%d ", syscall.Getpid())) 15 | } 16 | 17 | func main() { 18 | 19 | // Get the listener and ppid from the environment. If this is successful, 20 | // this process is a child that's inheriting and open listener from ppid. 21 | l, ppid, err := goagain.GetEnvs() 22 | if nil != err { 23 | 24 | // Listen on a TCP or a UNIX domain socket (TCP here). 25 | l, err = net.Listen("tcp", "127.0.0.1:48879") 26 | if nil != err { 27 | log.Fatalln(err) 28 | } 29 | log.Println("listening on", l.Addr()) 30 | 31 | // Accept connections in a new goroutine. 32 | go serve(l) 33 | 34 | } else { 35 | 36 | // Resume listening and accepting connections in a new goroutine. 37 | log.Println("resuming listening on", l.Addr()) 38 | go serve(l) 39 | 40 | // Kill the parent, now that the child has started successfully. 41 | if err := goagain.KillParent(ppid); nil != err { 42 | log.Fatalln(err) 43 | } 44 | 45 | } 46 | 47 | // Block the main goroutine awaiting signals. 48 | if err := goagain.AwaitSignals(l); nil != err { 49 | log.Fatalln(err) 50 | } 51 | 52 | // Do whatever's necessary to ensure a graceful exit like waiting for 53 | // goroutines to terminate or a channel to become closed. 54 | 55 | // In this case, we'll simply stop listening and wait one second. 56 | if err := l.Close(); nil != err { 57 | log.Fatalln(err) 58 | } 59 | time.Sleep(1e9) 60 | 61 | } 62 | 63 | func serve(l net.Listener) { 64 | for { 65 | c, err := l.Accept() 66 | if nil != err { 67 | if goagain.IsErrClosing(err) { 68 | break 69 | } 70 | log.Fatalln(err) 71 | } 72 | c.Write([]byte("Hello, world!\n")) 73 | c.Close() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/sendto/.gitignore: -------------------------------------------------------------------------------- 1 | sendto 2 | -------------------------------------------------------------------------------- /example/single/.gitignore: -------------------------------------------------------------------------------- 1 | single 2 | -------------------------------------------------------------------------------- /example/single/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/rcrowley/goagain" 5 | "fmt" 6 | "log" 7 | "net" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func init() { 13 | log.SetFlags(log.Lmicroseconds | log.Lshortfile) 14 | log.SetPrefix(fmt.Sprintf("pid:%d ", syscall.Getpid())) 15 | } 16 | 17 | func main() { 18 | 19 | // Inherit a net.Listener from our parent process or listen anew. 20 | l, err := goagain.Listener() 21 | if nil != err { 22 | 23 | // Listen on a TCP or a UNIX domain socket (TCP here). 24 | l, err = net.Listen("tcp", "127.0.0.1:48879") 25 | if nil != err { 26 | log.Fatalln(err) 27 | } 28 | log.Println("listening on", l.Addr()) 29 | 30 | // Accept connections in a new goroutine. 31 | go serve(l) 32 | 33 | } else { 34 | 35 | // Resume accepting connections in a new goroutine. 36 | log.Println("resuming listening on", l.Addr()) 37 | go serve(l) 38 | 39 | // Kill the parent, now that the child has started successfully. 40 | if err := goagain.Kill(); nil != err { 41 | log.Fatalln(err) 42 | } 43 | 44 | } 45 | 46 | // Block the main goroutine awaiting signals. 47 | if _, err := goagain.Wait(l); nil != err { 48 | log.Fatalln(err) 49 | } 50 | 51 | // Do whatever's necessary to ensure a graceful exit like waiting for 52 | // goroutines to terminate or a channel to become closed. 53 | // 54 | // In this case, we'll simply stop listening and wait one second. 55 | if err := l.Close(); nil != err { 56 | log.Fatalln(err) 57 | } 58 | time.Sleep(1e9) 59 | 60 | } 61 | 62 | // A very rude server that says hello and then closes your connection. 63 | func serve(l net.Listener) { 64 | for { 65 | c, err := l.Accept() 66 | if nil != err { 67 | if goagain.IsErrClosing(err) { 68 | break 69 | } 70 | log.Fatalln(err) 71 | } 72 | c.Write([]byte("Hello, world!\n")) 73 | c.Close() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /goagain.go: -------------------------------------------------------------------------------- 1 | // Zero-downtime restarts in Go. 2 | package goagain 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "os/signal" 12 | "reflect" 13 | "syscall" 14 | ) 15 | 16 | type strategy int 17 | 18 | const ( 19 | // The Single-exec strategy: parent forks child to exec with an inherited 20 | // net.Listener; child kills parent and becomes a child of init(8). 21 | Single strategy = iota 22 | 23 | // The Double-exec strategy: parent forks child to exec (first) with an 24 | // inherited net.Listener; child signals parent to exec (second); parent 25 | // kills child. 26 | Double 27 | ) 28 | 29 | // Don't make the caller import syscall. 30 | const ( 31 | SIGINT = syscall.SIGINT 32 | SIGQUIT = syscall.SIGQUIT 33 | SIGTERM = syscall.SIGTERM 34 | SIGUSR2 = syscall.SIGUSR2 35 | ) 36 | 37 | var ( 38 | // OnSIGHUP is the function called when the server receives a SIGHUP 39 | // signal. The normal use case for SIGHUP is to reload the 40 | // configuration. 41 | OnSIGHUP func(l net.Listener) error 42 | 43 | // OnSIGUSR1 is the function called when the server receives a 44 | // SIGUSR1 signal. The normal use case for SIGUSR1 is to repon the 45 | // log files. 46 | OnSIGUSR1 func(l net.Listener) error 47 | 48 | // The strategy to use; Single by default. 49 | Strategy strategy = Single 50 | ) 51 | 52 | // Re-exec this same image without dropping the net.Listener. 53 | func Exec(l net.Listener) error { 54 | var pid int 55 | fmt.Sscan(os.Getenv("GOAGAIN_PID"), &pid) 56 | if syscall.Getppid() == pid { 57 | return fmt.Errorf("goagain.Exec called by a child process") 58 | } 59 | argv0, err := lookPath() 60 | if nil != err { 61 | return err 62 | } 63 | if _, err := setEnvs(l); nil != err { 64 | return err 65 | } 66 | if err := os.Setenv( 67 | "GOAGAIN_SIGNAL", 68 | fmt.Sprintf("%d", syscall.SIGQUIT), 69 | ); nil != err { 70 | return err 71 | } 72 | log.Println("re-executing", argv0) 73 | return syscall.Exec(argv0, os.Args, os.Environ()) 74 | } 75 | 76 | // Fork and exec this same image without dropping the net.Listener. 77 | func ForkExec(l net.Listener) error { 78 | argv0, err := lookPath() 79 | if nil != err { 80 | return err 81 | } 82 | wd, err := os.Getwd() 83 | if nil != err { 84 | return err 85 | } 86 | fd, err := setEnvs(l) 87 | if nil != err { 88 | return err 89 | } 90 | if err := os.Setenv("GOAGAIN_PID", ""); nil != err { 91 | return err 92 | } 93 | if err := os.Setenv( 94 | "GOAGAIN_PPID", 95 | fmt.Sprint(syscall.Getpid()), 96 | ); nil != err { 97 | return err 98 | } 99 | var sig syscall.Signal 100 | if Double == Strategy { 101 | sig = syscall.SIGUSR2 102 | } else { 103 | sig = syscall.SIGQUIT 104 | } 105 | if err := os.Setenv("GOAGAIN_SIGNAL", fmt.Sprintf("%d", sig)); nil != err { 106 | return err 107 | } 108 | files := make([]*os.File, fd+1) 109 | files[syscall.Stdin] = os.Stdin 110 | files[syscall.Stdout] = os.Stdout 111 | files[syscall.Stderr] = os.Stderr 112 | addr := l.Addr() 113 | files[fd] = os.NewFile( 114 | fd, 115 | fmt.Sprintf("%s:%s->", addr.Network(), addr.String()), 116 | ) 117 | p, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ 118 | Dir: wd, 119 | Env: os.Environ(), 120 | Files: files, 121 | Sys: &syscall.SysProcAttr{}, 122 | }) 123 | if nil != err { 124 | return err 125 | } 126 | log.Println("spawned child", p.Pid) 127 | if err = os.Setenv("GOAGAIN_PID", fmt.Sprint(p.Pid)); nil != err { 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | // Test whether an error is equivalent to net.errClosing as returned by 134 | // Accept during a graceful exit. 135 | func IsErrClosing(err error) bool { 136 | if opErr, ok := err.(*net.OpError); ok { 137 | err = opErr.Err 138 | } 139 | return "use of closed network connection" == err.Error() 140 | } 141 | 142 | // Kill process specified in the environment with the signal specified in the 143 | // environment; default to SIGQUIT. 144 | func Kill() error { 145 | var ( 146 | pid int 147 | sig syscall.Signal 148 | ) 149 | _, err := fmt.Sscan(os.Getenv("GOAGAIN_PID"), &pid) 150 | if io.EOF == err { 151 | _, err = fmt.Sscan(os.Getenv("GOAGAIN_PPID"), &pid) 152 | } 153 | if nil != err { 154 | return err 155 | } 156 | if _, err := fmt.Sscan(os.Getenv("GOAGAIN_SIGNAL"), &sig); nil != err { 157 | sig = syscall.SIGQUIT 158 | } 159 | if syscall.SIGQUIT == sig && Double == Strategy { 160 | go syscall.Wait4(pid, nil, 0, nil) 161 | } 162 | log.Println("sending signal", sig, "to process", pid) 163 | return syscall.Kill(pid, sig) 164 | } 165 | 166 | // Reconstruct a net.Listener from a file descriptior and name specified in the 167 | // environment. Deal with Go's insistence on dup(2)ing file descriptors. 168 | func Listener() (l net.Listener, err error) { 169 | var fd uintptr 170 | if _, err = fmt.Sscan(os.Getenv("GOAGAIN_FD"), &fd); nil != err { 171 | return 172 | } 173 | l, err = net.FileListener(os.NewFile(fd, os.Getenv("GOAGAIN_NAME"))) 174 | if nil != err { 175 | return 176 | } 177 | switch l.(type) { 178 | case *net.TCPListener, *net.UnixListener: 179 | default: 180 | err = fmt.Errorf( 181 | "file descriptor is %T not *net.TCPListener or *net.UnixListener", 182 | l, 183 | ) 184 | return 185 | } 186 | if err = syscall.Close(int(fd)); nil != err { 187 | return 188 | } 189 | return 190 | } 191 | 192 | // Block this goroutine awaiting signals. Signals are handled as they 193 | // are by Nginx and Unicorn: . 194 | func Wait(l net.Listener) (syscall.Signal, error) { 195 | ch := make(chan os.Signal, 2) 196 | signal.Notify( 197 | ch, 198 | syscall.SIGHUP, 199 | syscall.SIGINT, 200 | syscall.SIGQUIT, 201 | syscall.SIGTERM, 202 | syscall.SIGUSR1, 203 | syscall.SIGUSR2, 204 | ) 205 | forked := false 206 | for { 207 | sig := <-ch 208 | log.Println(sig.String()) 209 | switch sig { 210 | 211 | // SIGHUP should reload configuration. 212 | case syscall.SIGHUP: 213 | if nil != OnSIGHUP { 214 | if err := OnSIGHUP(l); nil != err { 215 | log.Println("OnSIGHUP:", err) 216 | } 217 | } 218 | 219 | // SIGINT should exit. 220 | case syscall.SIGINT: 221 | return syscall.SIGINT, nil 222 | 223 | // SIGQUIT should exit gracefully. 224 | case syscall.SIGQUIT: 225 | return syscall.SIGQUIT, nil 226 | 227 | // SIGTERM should exit. 228 | case syscall.SIGTERM: 229 | return syscall.SIGTERM, nil 230 | 231 | // SIGUSR1 should reopen logs. 232 | case syscall.SIGUSR1: 233 | if nil != OnSIGUSR1 { 234 | if err := OnSIGUSR1(l); nil != err { 235 | log.Println("OnSIGUSR1:", err) 236 | } 237 | } 238 | 239 | // SIGUSR2 forks and re-execs the first time it is received and execs 240 | // without forking from then on. 241 | case syscall.SIGUSR2: 242 | if forked { 243 | return syscall.SIGUSR2, nil 244 | } 245 | forked = true 246 | if err := ForkExec(l); nil != err { 247 | return syscall.SIGUSR2, err 248 | } 249 | 250 | } 251 | } 252 | } 253 | 254 | func lookPath() (argv0 string, err error) { 255 | argv0, err = exec.LookPath(os.Args[0]) 256 | if nil != err { 257 | return 258 | } 259 | if _, err = os.Stat(argv0); nil != err { 260 | return 261 | } 262 | return 263 | } 264 | 265 | func setEnvs(l net.Listener) (fd uintptr, err error) { 266 | v := reflect.ValueOf(l).Elem().FieldByName("fd").Elem() 267 | fd = uintptr(v.FieldByName("sysfd").Int()) 268 | _, _, e1 := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_SETFD, 0) 269 | if 0 != e1 { 270 | err = e1 271 | return 272 | } 273 | if err = os.Setenv("GOAGAIN_FD", fmt.Sprint(fd)); nil != err { 274 | return 275 | } 276 | addr := l.Addr() 277 | if err = os.Setenv( 278 | "GOAGAIN_NAME", 279 | fmt.Sprintf("%s:%s->", addr.Network(), addr.String()), 280 | ); nil != err { 281 | return 282 | } 283 | return 284 | } 285 | -------------------------------------------------------------------------------- /legacy.go: -------------------------------------------------------------------------------- 1 | package goagain 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // Block this goroutine awaiting signals. Signals are handled as they 11 | // are by Nginx and Unicorn: . 12 | func AwaitSignals(l net.Listener) (err error) { 13 | _, err = Wait(l) 14 | return 15 | } 16 | 17 | // Convert and validate the GOAGAIN_FD, GOAGAIN_NAME, and GOAGAIN_PPID 18 | // environment variables. If all three are present and in order, this 19 | // is a child process that may pick up where the parent left off. 20 | func GetEnvs() (l net.Listener, ppid int, err error) { 21 | if _, err = fmt.Sscan(os.Getenv("GOAGAIN_PPID"), &ppid); nil != err { 22 | return 23 | } 24 | l, err = Listener() 25 | return 26 | } 27 | 28 | // Send SIGQUIT to the given ppid in order to complete the handoff to the 29 | // child process. 30 | func KillParent(ppid int) error { 31 | return syscall.Kill(ppid, syscall.SIGQUIT) 32 | } 33 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | set -e -x 2 | 3 | cd "$(dirname "$0")" 4 | 5 | findproc() { 6 | set +x 7 | find "/proc" -mindepth 2 -maxdepth 2 -name "exe" -lname "$PWD/$1" 2>"/dev/null" | 8 | cut -d"/" -f"3" 9 | set -x 10 | } 11 | 12 | for NAME in "legacy" "single" 13 | do 14 | cd "example/$NAME" 15 | go build 16 | ./$NAME & 17 | PID="$!" 18 | [ "$PID" -a -d "/proc/$PID" ] 19 | for _ in _ _ 20 | do 21 | OLDPID="$PID" 22 | sleep 1 23 | kill -USR2 "$PID" 24 | sleep 2 25 | PID="$(findproc "$NAME")" 26 | [ ! -d "/proc/$OLDPID" -a "$PID" -a -d "/proc/$PID" ] 27 | done 28 | [ "$(nc "127.0.0.1" "48879")" = "Hello, world!" ] 29 | kill -TERM "$PID" 30 | sleep 2 31 | [ ! -d "/proc/$PID" ] 32 | [ -z "$(findproc "$NAME")" ] 33 | cd "$OLDPWD" 34 | done 35 | 36 | cd "example/double" 37 | go build 38 | ./double & 39 | PID="$!" 40 | [ "$PID" -a -d "/proc/$PID" ] 41 | for _ in _ _ 42 | do 43 | sleep 1 44 | kill -USR2 "$PID" 45 | sleep 3 46 | NEWPID="$(findproc "double")" 47 | [ "$NEWPID" = "$PID" -a -d "/proc/$PID" ] 48 | done 49 | [ "$(nc "127.0.0.1" "48879")" = "Hello, world!" ] 50 | kill -TERM "$PID" 51 | sleep 3 52 | [ ! -d "/proc/$PID" ] 53 | [ -z "$(findproc "double")" ] 54 | cd "$OLDPWD" 55 | --------------------------------------------------------------------------------