├── .gitignore ├── examples └── echo │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── echo.go ├── .travis.yml ├── Makefile ├── config.go ├── go.mod ├── log.go ├── err.go ├── .golangci.yml ├── util_test.go ├── README.md ├── pid.go ├── go.sum ├── upzip.go ├── util.go ├── borrow.go ├── upgrade.go ├── gracego.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | -------------------------------------------------------------------------------- /examples/echo/.gitignore: -------------------------------------------------------------------------------- 1 | echo 2 | echo.zip 3 | v* 4 | build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.12" 5 | 6 | script: 7 | - make check && [[ -z `git status -s` ]] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | golangci-lint run 3 | 4 | format: 5 | goimports -w -l . 6 | go fmt ./... 7 | 8 | test: 9 | go test ./... 10 | 11 | all: format lint test -------------------------------------------------------------------------------- /examples/echo/Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -rf build 3 | mkdir build 4 | 5 | build: clean 6 | go build -o build/echo 7 | 8 | zip: build 9 | rm -f echo.zip 10 | cd build; zip echo.zip echo 11 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | var addrInUseWaitSecond = 5 7 | 8 | func SetAddrInUseWaitSecond(seconds int) { 9 | addrInUseWaitSecond = seconds 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vogo/gracego 2 | 3 | go 1.20 4 | 5 | require github.com/stretchr/testify v1.8.2 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | ) 10 | 11 | func graceLog(format string, args ...interface{}) { 12 | log.Println("GRAC", serverID, fmt.Sprintf(format, args...)) 13 | } 14 | -------------------------------------------------------------------------------- /err.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "net" 8 | "os" 9 | "syscall" 10 | ) 11 | 12 | func IsAddrUsedErr(err error) bool { 13 | opErr, ok := err.(*net.OpError) 14 | if !ok { 15 | return false 16 | } 17 | callErr, ok := opErr.Err.(*os.SyscallError) 18 | if !ok { 19 | return false 20 | } 21 | return callErr.Err == syscall.EADDRINUSE 22 | } 23 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | # echo examples 2 | 3 | examples for gracefully restart, upgrade through http request. 4 | 5 | ```bash 6 | git clone https://github.com/vogo/gracego.git 7 | cd gracego/examples/echo 8 | make zip 9 | 10 | cd build 11 | 12 | # hard link 13 | ln echo myecho 14 | 15 | # start echo server 16 | ./myecho 17 | 18 | # start a new server to replace the old 19 | ./myecho 20 | 21 | # restart through signal 22 | ps -ef |grep -v grep |grep echo 23 | kill -HUP 24 | 25 | # sleep 5s request 26 | curl http://127.0.0.1:8081/sleep5s 27 | 28 | # calculate 5s request 29 | curl http://127.0.0.1:8081/calcuate5s 30 | 31 | # upgrade through http request 32 | curl http://127.0.0.1:8081/upgrade 33 | ``` -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: true 4 | golint: 5 | min-confidence: 0 6 | gocyclo: 7 | min-complexity: 10 8 | maligned: 9 | suggest-new: true 10 | dupl: 11 | threshold: 100 12 | goconst: 13 | min-len: 2 14 | min-occurrences: 2 15 | misspell: 16 | locale: US 17 | lll: 18 | line-length: 140 19 | goimports: 20 | local-prefixes: github.com/golangci/golangci-lint 21 | gocritic: 22 | enabled-tags: 23 | - performance 24 | - style 25 | - experimental 26 | disabled-checks: 27 | - wrapperFunc 28 | 29 | linters: 30 | enable-all: true 31 | disable: 32 | - prealloc 33 | - gochecknoglobals 34 | 35 | run: 36 | skip-dirs: 37 | - test/testdata_etc 38 | - pkg/golinters/goanalysis/(checker|passes) 39 | 40 | issues: 41 | exclude-rules: 42 | - text: "weak cryptographic primitive" 43 | linters: 44 | - gosec 45 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFilePath(t *testing.T) { 14 | assert.Equal(t, ".", filepath.Dir("test")) 15 | assert.Equal(t, ".", filepath.Dir(".")) 16 | assert.Equal(t, ".", filepath.Dir("./")) 17 | assert.Equal(t, ".", filepath.Dir("./test")) 18 | assert.Equal(t, "..", filepath.Dir("./../test")) 19 | assert.Equal(t, "../..", filepath.Dir("./../../test")) 20 | 21 | assert.Equal(t, "/a/b/test", filepath.Clean("/a/b/test")) 22 | assert.Equal(t, "/a/b/test", filepath.Clean("/a/b/test/")) 23 | assert.Equal(t, "/test", filepath.Clean("/a/b/../../test")) 24 | 25 | assert.Equal(t, "test", filepath.Clean("test")) 26 | assert.Equal(t, "test", filepath.Clean("./test")) 27 | assert.Equal(t, "../test", filepath.Clean("./../test")) 28 | assert.Equal(t, "../../test", filepath.Clean("./../../test")) 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `gracego` enables gracefully restart, upgrade or replace golang application. 2 | 3 | ## Usage 4 | 5 | ### Start server 6 | Your server must implement the interface `GraceServer` with two methods: 7 | ```go 8 | type GraceServer interface { 9 | Serve(listener net.Listener) error 10 | Shutdown(ctx context.Context) error 11 | } 12 | ``` 13 | 14 | Use `gracego.Serve()` to start your server: 15 | ```go 16 | func main() { 17 | server = &http.Server{} 18 | 19 | err := gracego.Serve(server, "demo", ":8080") 20 | if err != nil { 21 | fmt.Printf("server error: %v", err) 22 | } 23 | } 24 | ``` 25 | 26 | ### Restart server gracefully 27 | 28 | ```bash 29 | kill -HUP 30 | ``` 31 | 32 | ### Upgrade server gracefully 33 | 34 | - `v2`: the new version to upgrade 35 | - `echo`: the relative path of the upgrade command in the download.zip 36 | - `http://127.0.0.1:8081/download.zip`: the upgrade url, which must be a zip file and end with `.zip` or `.jar`. 37 | ```go 38 | 39 | err := gracego.Upgrade("v2", "echo", "http://127.0.0.1:8081/download.zip") 40 | if err != nil { 41 | // error handle 42 | } 43 | ``` 44 | 45 | ## Examples 46 | 47 | - [echo](examples/echo/README.md): example to shutdown, restart, upgrade, replace application gracefully 48 | -------------------------------------------------------------------------------- /pid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | enableWritePid = false 14 | pidFileDir string 15 | pidFilePath string 16 | ) 17 | 18 | // EnableWritePid enable to write pid file 19 | // dir - the directory where to write pid file 20 | func EnableWritePid(dir string) error { 21 | if dir == "" { 22 | dir = os.TempDir() 23 | } else if _, err := os.Stat(dir); err != nil { 24 | return err 25 | } 26 | 27 | if !strings.HasSuffix(dir, string(os.PathSeparator)) { 28 | dir += string(os.PathSeparator) 29 | } 30 | 31 | pidFileDir = dir 32 | 33 | enableWritePid = true 34 | return nil 35 | } 36 | 37 | func writePidFile() { 38 | if !enableWritePid { 39 | return 40 | } 41 | 42 | if pidFilePath == "" { 43 | pidFilePath = fmt.Sprintf("%s%s.pid", pidFileDir, serverName) 44 | graceLog("set pid file: %s", pidFilePath) 45 | } 46 | 47 | pidFile, err := os.OpenFile(pidFilePath, os.O_RDWR, 0660) 48 | if err != nil { 49 | pidFile, err = os.Create(pidFilePath) 50 | if err != nil { 51 | graceLog("failed to create pid file %s, error: %v", pidFilePath, err) 52 | return 53 | } 54 | } 55 | defer pidFile.Close() 56 | 57 | pid := fmt.Sprint(os.Getpid()) 58 | _, err = pidFile.WriteString(pid) 59 | if err != nil { 60 | graceLog("failed to write pid file %s, error: %v", pidFilePath, err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 12 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /upzip.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "archive/zip" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // unzip will decompress a zip archive, moving all files and folders 15 | // within the zip file (parameter 1) to an output directory (parameter 2). 16 | func unzip(src, dest string) error { 17 | r, err := zip.OpenReader(src) 18 | if err != nil { 19 | return err 20 | } 21 | defer r.Close() 22 | 23 | // Make File 24 | if err := os.MkdirAll(dest, os.ModePerm); err != nil { 25 | return err 26 | } 27 | 28 | for _, zipFile := range r.File { 29 | // Check for ZipSlip 30 | fileName := strings.ReplaceAll(zipFile.Name, "..", "") 31 | 32 | if fileName != zipFile.Name { 33 | graceLog("ignore zip file: %s", zipFile.Name) 34 | continue 35 | } 36 | 37 | targetPath := filepath.Join(dest, fileName) 38 | 39 | if err := writeZipFile(targetPath, zipFile); err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func writeZipFile(targetPath string, f *zip.File) error { 47 | if f.FileInfo().IsDir() { 48 | if err := os.MkdirAll(targetPath, os.ModePerm); err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | outFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | rc, err := f.Open() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | _, err = io.Copy(outFile, rc) 65 | 66 | outFile.Close() 67 | _ = rc.Close() 68 | 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | func existFile(file string) bool { 18 | if _, err := os.Stat(file); err == nil { 19 | return true 20 | } else if os.IsNotExist(err) { 21 | return false 22 | } 23 | return false 24 | } 25 | 26 | func execCmd(cmdline string) ([]byte, error) { 27 | cmd := exec.Command("/bin/sh", "-c", cmdline) 28 | result, err := cmd.CombinedOutput() 29 | if err != nil { 30 | return nil, err 31 | } 32 | return bytes.ReplaceAll(result, []byte{'\n'}, nil), nil 33 | } 34 | 35 | func getPidFromAddr(addr string) (int, error) { 36 | idx := strings.LastIndex(addr, ":") 37 | if idx < 0 { 38 | return 0, errors.New("can't get port from address") 39 | } 40 | port := addr[idx+1:] 41 | 42 | result, err := execCmd(fmt.Sprintf("lsof -i:%s |grep LISTEN | tail -1 |awk '{print $2}'", port)) 43 | if err != nil { 44 | return 0, fmt.Errorf("failed to get pid from port %s, error: %+v", port, err) 45 | } 46 | pid, err := strconv.Atoi(string(result)) 47 | if err != nil { 48 | return 0, fmt.Errorf("failed to get pid from port %s, result: %s, error: %v", port, result, err) 49 | } 50 | return pid, nil 51 | } 52 | 53 | func checkSameBinProcess(pid int) (string, bool) { 54 | cmdline, err := getCommandline(pid) 55 | if err != nil { 56 | graceLog("can't get command line for pid %d, error: %v", pid, err) 57 | return "", false 58 | } 59 | if strings.Contains(cmdline, serverBin) { 60 | return cmdline, true 61 | } 62 | return cmdline, false 63 | } 64 | 65 | func getCommandline(pid int) (string, error) { 66 | linuxCmdlineFile := fmt.Sprintf("/proc/%d/cmdline", pid) 67 | if existFile(linuxCmdlineFile) { 68 | data, err := ioutil.ReadFile(linuxCmdlineFile) 69 | if err != nil { 70 | return "", err 71 | } 72 | return string(data), nil 73 | } 74 | 75 | result, err := execCmd(fmt.Sprintf("ps -o 'command' -p %d |tail -1", pid)) 76 | if err != nil { 77 | return "", err 78 | } 79 | return string(result), nil 80 | } 81 | -------------------------------------------------------------------------------- /examples/echo/echo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "log" 9 | "math" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | 15 | "github.com/vogo/gracego" 16 | ) 17 | 18 | var ( 19 | server *http.Server 20 | listenAddr = ":8081" 21 | ) 22 | 23 | func main() { 24 | http.HandleFunc("/hello", HelloHandler) 25 | http.HandleFunc("/sleep5s", SleepHandler) 26 | http.HandleFunc("/calculate5s", CalculateHandler) 27 | http.HandleFunc("/download.zip", DownloadHandler) 28 | http.HandleFunc("/upgrade", UpgradeHandler) 29 | 30 | server = &http.Server{} 31 | 32 | err := gracego.EnableWritePid("/tmp") 33 | if err != nil { 34 | fmt.Printf("write pid error: %v", err) 35 | } 36 | 37 | err = gracego.Serve(server, "echo", listenAddr) 38 | if err != nil { 39 | fmt.Printf("server error: %v", err) 40 | } 41 | } 42 | 43 | // HelloHandler handle hello request 44 | func HelloHandler(w http.ResponseWriter, r *http.Request) { 45 | info("request hello") 46 | response(w, 200, "world") 47 | } 48 | 49 | // SleepHandler handle sleep request 50 | func SleepHandler(w http.ResponseWriter, r *http.Request) { 51 | info("request sleep") 52 | time.Sleep(5 * time.Second) 53 | response(w, 200, "world") 54 | } 55 | 56 | // CalculateHandler handle calculation request 57 | func CalculateHandler(w http.ResponseWriter, r *http.Request) { 58 | info("request calculate") 59 | fiveSecondCalc() 60 | response(w, 200, "world") 61 | } 62 | 63 | // the calculation will cost about 5.89s for 2.3 GHz Intel Core i5 64 | func fiveSecondCalc() { 65 | for i := 0; i < math.MaxInt16; i++ { 66 | for j := 0; j < 1<<13; j++ { 67 | math.Sin(float64(i)) 68 | math.Cos(float64(i)) 69 | } 70 | } 71 | } 72 | 73 | // DownloadHandler download the graceup server zip 74 | func DownloadHandler(w http.ResponseWriter, r *http.Request) { 75 | info("request download") 76 | path, err := os.Executable() 77 | if err != nil { 78 | responseError(w, err) 79 | return 80 | } 81 | 82 | dir := filepath.Dir(path) 83 | zipFilePath := filepath.Join(dir, "echo.zip") 84 | file, err := os.OpenFile(zipFilePath, os.O_RDONLY, os.ModePerm) 85 | if err != nil { 86 | responseError(w, err) 87 | return 88 | } 89 | 90 | w.Header().Add("content-type", "application/octet-stream") 91 | _, err = io.Copy(w, file) 92 | if err != nil { 93 | responseError(w, err) 94 | return 95 | } 96 | } 97 | 98 | // UpgradeHandler restart server 99 | func UpgradeHandler(w http.ResponseWriter, r *http.Request) { 100 | info("request upgrade") 101 | err := gracego.Upgrade("v2", "echo", "http://127.0.0.1"+listenAddr+"/download.zip") 102 | if err != nil { 103 | responseError(w, err) 104 | } 105 | response(w, 200, "success") 106 | } 107 | 108 | func responseError(w http.ResponseWriter, err error) { 109 | response(w, 500, err.Error()) 110 | } 111 | 112 | func response(w http.ResponseWriter, code int, msg string) { 113 | w.WriteHeader(code) 114 | _, _ = w.Write([]byte(msg)) 115 | } 116 | 117 | func info(format string, args ...interface{}) { 118 | log.Println(gracego.GetServerID(), "-", fmt.Sprintf(format, args...)) 119 | } 120 | -------------------------------------------------------------------------------- /borrow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os" 10 | "path/filepath" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func getBorrowSockFile(pid int) string { 16 | return filepath.Join(os.TempDir(), fmt.Sprintf("gracego_borrow_%d.sock", pid)) 17 | } 18 | 19 | func borrow(addr string) (*os.File, error) { 20 | pid, err := getPidFromAddr(addr) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | procInfo, ok := checkSameBinProcess(pid) 26 | if !ok { 27 | return nil, fmt.Errorf("addr %s hold by process: %s", addr, procInfo) 28 | } 29 | 30 | graceLog("borrow addr %s from process[%d]: %s", addr, pid, procInfo) 31 | 32 | return borrowFromPid(pid) 33 | } 34 | 35 | func borrowFromPid(pid int) (*os.File, error) { 36 | proc, err := os.FindProcess(pid) 37 | if err != nil { 38 | return nil, fmt.Errorf("can't find target process %d, error: %+v", pid, err) 39 | } 40 | 41 | err = proc.Signal(syscall.SIGUSR1) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to send signal to process %d, error: %+v", pid, err) 44 | } 45 | 46 | borrowSockFile := getBorrowSockFile(pid) 47 | 48 | ticker := time.NewTicker(time.Millisecond * 100) 49 | for i := 0; i < 20 && !existFile(borrowSockFile); i++ { 50 | <-ticker.C 51 | } 52 | ticker.Stop() 53 | 54 | borrowConn, err := net.Dial("unix", borrowSockFile) 55 | if err != nil { 56 | return nil, err 57 | } 58 | defer borrowConn.Close() 59 | 60 | go func() { 61 | // wait 2 seconds (timeout control) 62 | <-time.After(time.Second * 2) 63 | borrowConn.Close() 64 | }() 65 | 66 | sendFdConn := borrowConn.(*net.UnixConn) 67 | sockFile, err := sendFdConn.File() 68 | if err != nil { 69 | return nil, err 70 | } 71 | defer sockFile.Close() 72 | 73 | buf := make([]byte, syscall.CmsgSpace(4)) 74 | if _, _, _, _, err = syscall.Recvmsg(int(sockFile.Fd()), nil, buf, 0); err != nil { 75 | return nil, err 76 | } 77 | 78 | var messages []syscall.SocketControlMessage 79 | messages, err = syscall.ParseSocketControlMessage(buf) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | fds, err := syscall.ParseUnixRights(&messages[0]) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return os.NewFile(uintptr(fds[0]), ""), nil 90 | } 91 | 92 | func borrowSend() error { 93 | listenFile, err := listenFile() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | borrowSockFile := getBorrowSockFile(os.Getpid()) 99 | sockListener, err := net.Listen("unix", borrowSockFile) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | go func() { 105 | // wait 2 seconds (timeout control) 106 | <-time.After(time.Second * 2) 107 | sockListener.Close() 108 | }() 109 | 110 | sockConn, err := sockListener.Accept() 111 | if err != nil { 112 | return err 113 | } 114 | defer sockConn.Close() 115 | 116 | conn := sockConn.(*net.UnixConn) 117 | sockFile, err := conn.File() 118 | if err != nil { 119 | return err 120 | } 121 | defer sockFile.Close() 122 | 123 | rights := syscall.UnixRights(int(listenFile.Fd())) 124 | err = syscall.Sendmsg(int(sockFile.Fd()), nil, rights, nil, 0) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | time.Sleep(time.Second) 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /upgrade.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | // Upgrade gracefully upgrade server 18 | // - version: the new version of the server 19 | // - path: the relative path of the command in the upgrade compress file 20 | // - upgradeUrl: the url of the upgrade file, which must be a zip format file with a suffix `.jar` or `.zip` 21 | func Upgrade(version, path, upgradeURL string) error { 22 | if err := upgradeServerCmd(version, path, upgradeURL); err != nil { 23 | return err 24 | } 25 | go restart() 26 | return nil 27 | } 28 | 29 | // upgradeServerCmd grace up server bin file 30 | func upgradeServerCmd(version, path, upgradeURL string) error { 31 | if server == nil || serverBin == "" { 32 | return errors.New("server not started") 33 | } 34 | 35 | versionDir := filepath.Join(serverDir, version) 36 | err := os.Mkdir(versionDir, 0770) 37 | if err != nil && !strings.Contains(err.Error(), "file exists") { 38 | return err 39 | } 40 | 41 | fileName, err := parseFileName(upgradeURL) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | upgradeCmd := filepath.Join(versionDir, path) 47 | _, err = os.Open(upgradeCmd) 48 | if err == nil { 49 | graceLog("found upgrade command file: %s", upgradeCmd) 50 | return link(upgradeCmd, serverCmdPath) 51 | } 52 | 53 | downloadPath := filepath.Join(versionDir, fileName) 54 | err = downloadFile(downloadPath, upgradeURL) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | err = unzip(downloadPath, versionDir) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return link(upgradeCmd, serverCmdPath) 65 | } 66 | 67 | func parseFileName(upgradeURL string) (string, error) { 68 | u, err := url.Parse(upgradeURL) 69 | if err != nil { 70 | return "", err 71 | } 72 | uri := u.RequestURI() 73 | index := strings.LastIndex(uri, "/") 74 | if index < 0 { 75 | return "", fmt.Errorf("invalid download url: %s", u) 76 | } 77 | 78 | fileName := uri[index+1:] 79 | if !acceptFileSuffix(fileName) { 80 | return "", fmt.Errorf("invalid suffix for download url: %s", u) 81 | } 82 | return fileName, nil 83 | } 84 | 85 | func link(src, dest string) error { 86 | _ = os.Remove(dest) 87 | graceLog("link %s to %s", src, dest) 88 | return os.Link(src, dest) 89 | } 90 | 91 | func acceptFileSuffix(f string) bool { 92 | return strings.HasSuffix(f, ".jar") || strings.HasSuffix(f, ".zip") 93 | } 94 | 95 | // downloadFile will download a url to a local file. It's efficient because it will 96 | // write as it downloads and not load the whole file into memory. 97 | func downloadFile(filePath, upgradeURL string) error { 98 | u, err := url.Parse(upgradeURL) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | graceLog("download %s to %s", upgradeURL, filePath) 104 | _ = os.Remove(filePath) 105 | 106 | // Get the data 107 | resp, err := http.Get(u.String()) 108 | if err != nil { 109 | return err 110 | } 111 | defer resp.Body.Close() 112 | 113 | switch resp.StatusCode { 114 | case 200: 115 | case 404: 116 | return fmt.Errorf("file not found: %s", upgradeURL) 117 | default: 118 | buf := make([]byte, 1024) 119 | result := "" 120 | if n, readErr := resp.Body.Read(buf); n > 0 && readErr == nil { 121 | result = string(buf[:n]) 122 | } 123 | 124 | return fmt.Errorf("download failed, status code: %d, result: %s", resp.StatusCode, result) 125 | } 126 | 127 | // Create the file 128 | out, err := os.Create(filePath) 129 | if err != nil { 130 | graceLog("can't create file: %v", err) 131 | return err 132 | } 133 | defer out.Close() 134 | 135 | // Write the body to file 136 | _, err = io.Copy(out, resp.Body) 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /gracego.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The vogo Authors. All rights reserved. 2 | // author: wongoo 3 | 4 | package gracego 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "path/filepath" 15 | "reflect" 16 | "strings" 17 | "syscall" 18 | "time" 19 | ) 20 | 21 | const ( 22 | ForkCommandArg = "-grace-forked" 23 | ) 24 | 25 | var ( 26 | listener net.Listener 27 | server GraceServer 28 | graceForkArgs []string 29 | serverDir string 30 | serverBin string 31 | serverCmdPath string 32 | serverName string 33 | serverAddr string 34 | serverForked bool 35 | 36 | shutdownChan = make(chan error, 1) 37 | serverID = int(time.Now().Unix()) 38 | shutdownTimeout = 10 * time.Second 39 | ) 40 | 41 | // GraceServer serve net listener 42 | type GraceServer interface { 43 | Serve(listener net.Listener) error 44 | } 45 | 46 | // GraceShutdowner support shutdown 47 | type GraceShutdowner interface { 48 | Shutdown(ctx context.Context) error 49 | } 50 | 51 | // Shutdowner support shutdown 52 | type Shutdowner interface { 53 | Shutdown() error 54 | } 55 | 56 | // GetServerID get server id 57 | func GetServerID() int { 58 | return serverID 59 | } 60 | 61 | // SetShutdownTimeout set the server shutdown timeout duration 62 | func SetShutdownTimeout(d time.Duration) { 63 | if d > 0 { 64 | shutdownTimeout = d 65 | } 66 | } 67 | 68 | // Serve serve grace server 69 | func Serve(svr GraceServer, name, addr string) error { 70 | var err error 71 | serverCmdPath, err = os.Executable() 72 | if err != nil { 73 | return err 74 | } 75 | serverDir = filepath.Dir(serverCmdPath) 76 | 77 | serverAddr = addr 78 | serverName = name 79 | server = svr 80 | 81 | serverBin = os.Args[0] 82 | graceForkArgs = os.Args[1:] 83 | serverForked = false 84 | for _, arg := range graceForkArgs { 85 | if arg == ForkCommandArg { 86 | serverForked = true 87 | break 88 | } 89 | } 90 | if !serverForked { 91 | graceForkArgs = append(graceForkArgs, ForkCommandArg) 92 | } 93 | 94 | return serveServer() 95 | } 96 | 97 | // serveServer start grace server 98 | func serveServer() error { 99 | var err error 100 | 101 | writePidFile() 102 | 103 | if serverForked { 104 | graceLog("listen in forked child at %s, pid %d", serverAddr, os.Getpid()) 105 | 106 | f := os.NewFile(3, "") 107 | listener, err = net.FileListener(f) 108 | } else { 109 | graceLog("listen at %s, pid %d", serverAddr, os.Getpid()) 110 | listener, err = net.Listen("tcp", serverAddr) 111 | 112 | // wait for address being released 113 | if err != nil && IsAddrUsedErr(err) { 114 | f, borrowErr := borrow(serverAddr) 115 | if borrowErr != nil { 116 | graceLog("borrow fd fail: %v", borrowErr) 117 | graceLog("wait %d seconds to release address: %s", addrInUseWaitSecond, serverAddr) 118 | <-time.After(time.Second * time.Duration(addrInUseWaitSecond)) 119 | listener, err = net.Listen("tcp", serverAddr) 120 | } else { 121 | listener, err = net.FileListener(f) 122 | } 123 | } 124 | } 125 | 126 | if err != nil { 127 | graceLog("listen failed: %v", err) 128 | return err 129 | } 130 | 131 | go func() { 132 | err = server.Serve(listener) 133 | if err != nil { 134 | graceLog("server.Serve end! %v", err) 135 | } 136 | 137 | // close shutdown chan to stop signal waiting 138 | close(shutdownChan) 139 | }() 140 | 141 | handleSignal() 142 | graceLog("server end, pid %d", os.Getpid()) 143 | return nil 144 | } 145 | 146 | func handleSignal() { 147 | signalChan := make(chan os.Signal, 1) 148 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1) 149 | 150 | var sig os.Signal 151 | 152 | for { 153 | select { 154 | case sig = <-signalChan: 155 | case err := <-shutdownChan: 156 | if err != nil { 157 | graceLog("shutdown error: %v", err) 158 | } 159 | close(shutdownChan) 160 | return 161 | } 162 | 163 | graceLog("receive signal: %v", sig) 164 | 165 | switch sig { 166 | case syscall.SIGINT, syscall.SIGTERM: 167 | signal.Stop(signalChan) 168 | _ = Shutdown() 169 | return 170 | case syscall.SIGHUP: 171 | restart() 172 | return 173 | case syscall.SIGUSR1: 174 | graceLog("receive borrow listener request") 175 | if err := borrowSend(); err != nil { 176 | graceLog("borrow send error: %v", err) 177 | continue 178 | } 179 | 180 | // end server 181 | return 182 | } 183 | } 184 | } 185 | 186 | // Shutdown graceful server 187 | func Shutdown() error { 188 | if server == nil { 189 | return errors.New("server not start") 190 | } 191 | 192 | if enableWritePid { 193 | _ = os.Remove(pidFilePath) 194 | } 195 | 196 | go func() { 197 | shutdownChan <- shutdownServer(server) 198 | }() 199 | 200 | select { 201 | case <-time.After(shutdownTimeout + time.Second): 202 | shutdownChan <- fmt.Errorf("shutdown timeout over %d seconds", shutdownTimeout/time.Second) 203 | case <-shutdownChan: 204 | } 205 | 206 | return nil 207 | } 208 | 209 | func shutdownServer(s GraceServer) error { 210 | graceLog("start shutdown server %s", reflect.TypeOf(s)) 211 | defer graceLog("finish shutdown server %s", reflect.TypeOf(s)) 212 | switch st := s.(type) { 213 | case GraceShutdowner: 214 | ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) 215 | defer cancel() 216 | return st.Shutdown(ctx) 217 | case Shutdowner: 218 | return st.Shutdown() 219 | default: 220 | return errors.New("server shutdown unsupported") 221 | } 222 | } 223 | 224 | func restart() { 225 | err := fork() 226 | if err != nil { 227 | graceLog("failed to restart! fork child process error: %v", err) 228 | return 229 | } 230 | _ = Shutdown() 231 | } 232 | 233 | func fork() error { 234 | listenFile, err := listenFile() 235 | if err != nil { 236 | return err 237 | } 238 | 239 | graceLog("restart server %s: %s %s", serverName, serverBin, strings.Join(graceForkArgs, " ")) 240 | cmd := exec.Command(serverBin, graceForkArgs...) 241 | cmd.Stdout = os.Stdout 242 | cmd.Stderr = os.Stderr 243 | cmd.ExtraFiles = []*os.File{listenFile} 244 | return cmd.Start() 245 | } 246 | 247 | func listenFile() (f *os.File, err error) { 248 | tcpListener, ok := listener.(*net.TCPListener) 249 | if !ok { 250 | return nil, fmt.Errorf("listener is not tcp listener") 251 | } 252 | 253 | return tcpListener.File() 254 | } 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------