├── go.mod
├── cmd
└── go-selfupdate
│ ├── main_test.go
│ └── main.go
├── example
├── public
│ └── index.html
├── src
│ ├── example-server
│ │ └── main.go
│ └── hello-updater
│ │ └── main.go
└── run-example.sh
├── selfupdate
├── hide_noop.go
├── hide_windows.go
├── requester.go
├── selfupdate_test.go
└── selfupdate.go
├── go.sum
├── .gitignore
├── .github
└── workflows
│ └── ci.yml
├── LICENSE.md
└── README.md
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sanbornm/go-selfupdate
2 |
3 | go 1.15
4 |
5 | require github.com/kr/binarydist v0.1.0
6 |
--------------------------------------------------------------------------------
/cmd/go-selfupdate/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "testing"
4 |
5 | func TestUpdater(t *testing.T) {
6 | }
7 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
Hello Updater Example Server
3 | I serve updates of the hello-updater program!
4 |
--------------------------------------------------------------------------------
/selfupdate/hide_noop.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package selfupdate
5 |
6 | func hideFile(path string) error {
7 | return nil
8 | }
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
2 | github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Use glob syntax.
2 | syntax: glob
3 |
4 | *.DS_Store
5 | *.swp
6 | *.swo
7 | *.pyc
8 | *.php~
9 | *.orig
10 | *~
11 | *.db
12 | *.log
13 | public/*
14 | go-selfupdate/go-selfupdate
15 | example/example-server
16 | example/hello-updater
17 | example/public/*
18 | example/deployment/*
19 | example/go-selfupdate
20 | *cktime
21 |
--------------------------------------------------------------------------------
/selfupdate/hide_windows.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "syscall"
5 | "unsafe"
6 | )
7 |
8 | func hideFile(path string) error {
9 | kernel32 := syscall.NewLazyDLL("kernel32.dll")
10 | setFileAttributes := kernel32.NewProc("SetFileAttributesW")
11 |
12 | r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 2)
13 |
14 | if r1 == 0 {
15 | return err
16 | } else {
17 | return nil
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 |
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: 1.19
23 |
24 | - name: Build
25 | run: go build -v ./...
26 |
27 | - name: Test
28 | run: go test -v ./...
--------------------------------------------------------------------------------
/example/src/example-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "net/http"
7 | )
8 |
9 | var servePath = flag.String("dir", "./public", "path to serve")
10 |
11 | type logHandler struct {
12 | handler http.Handler
13 | }
14 |
15 | func (lh *logHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
16 | log.Printf("(example-server) received request %s\n", r.URL.RequestURI())
17 | lh.handler.ServeHTTP(rw, r)
18 | }
19 |
20 | func main() {
21 | log.SetFlags(log.LstdFlags | log.Lshortfile)
22 | flag.Parse()
23 |
24 | // Simple static webserver with logging:
25 | log.Printf("Starting HTTP server on :8080 serving path %q Ctrl + C to close and quit", *servePath)
26 | log.Fatal(http.ListenAndServe(":8080", &logHandler{
27 | handler: http.FileServer(http.Dir(*servePath))},
28 | ))
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Mark Sanborn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/selfupdate/requester.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | )
8 |
9 | // Requester interface allows developers to customize the method in which
10 | // requests are made to retrieve the version and binary.
11 | type Requester interface {
12 | Fetch(url string) (io.ReadCloser, error)
13 | }
14 |
15 | // HTTPRequester is the normal requester that is used and does an HTTP
16 | // to the URL location requested to retrieve the specified data.
17 | type HTTPRequester struct{}
18 |
19 | // Fetch will return an HTTP request to the specified url and return
20 | // the body of the result. An error will occur for a non 200 status code.
21 | func (httpRequester *HTTPRequester) Fetch(url string) (io.ReadCloser, error) {
22 | resp, err := http.Get(url)
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | if resp.StatusCode != 200 {
28 | return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status)
29 | }
30 |
31 | return resp.Body, nil
32 | }
33 |
34 | // mockRequester used for some mock testing to ensure the requester contract
35 | // works as specified.
36 | type mockRequester struct {
37 | currentIndex int
38 | fetches []func(string) (io.ReadCloser, error)
39 | }
40 |
41 | func (mr *mockRequester) handleRequest(requestHandler func(string) (io.ReadCloser, error)) {
42 | if mr.fetches == nil {
43 | mr.fetches = []func(string) (io.ReadCloser, error){}
44 | }
45 | mr.fetches = append(mr.fetches, requestHandler)
46 | }
47 |
48 | func (mr *mockRequester) Fetch(url string) (io.ReadCloser, error) {
49 | if len(mr.fetches) <= mr.currentIndex {
50 | return nil, fmt.Errorf("no for currentIndex %d to mock", mr.currentIndex)
51 | }
52 | current := mr.fetches[mr.currentIndex]
53 | mr.currentIndex++
54 |
55 | return current(url)
56 | }
57 |
--------------------------------------------------------------------------------
/example/src/hello-updater/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/sanbornm/go-selfupdate/selfupdate"
7 | )
8 |
9 | // The purpose of this app is to provide a simple example that just prints
10 | // its version and updates to the latest version from example-server
11 | // on localhost:8080.
12 |
13 | // the app's version. This will be set on build.
14 | var version string
15 |
16 | // go-selfupdate setup and config
17 | var updater = &selfupdate.Updater{
18 | CurrentVersion: version, // Manually update the const, or set it using `go build -ldflags="-X main.VERSION=" -o hello-updater src/hello-updater/main.go`
19 | ApiURL: "http://localhost:8080/", // The server hosting `$CmdName/$GOOS-$ARCH.json` which contains the checksum for the binary
20 | BinURL: "http://localhost:8080/", // The server hosting the zip file containing the binary application which is a fallback for the patch method
21 | DiffURL: "http://localhost:8080/", // The server hosting the binary patch diff for incremental updates
22 | Dir: "update/", // The directory created by the app when run which stores the cktime file
23 | CmdName: "hello-updater", // The app name which is appended to the ApiURL to look for an update
24 | ForceCheck: true, // For this example, always check for an update unless the version is "dev"
25 | }
26 |
27 | func main() {
28 | log.SetFlags(log.LstdFlags | log.Lshortfile)
29 |
30 | // print the current version
31 | log.Printf("(hello-updater) Hello world! I am currently version: %q", updater.CurrentVersion)
32 |
33 | // try to update
34 | err := updater.BackgroundRun()
35 | if err != nil {
36 | log.Fatalln("Failed to update app:", err)
37 | }
38 |
39 | // print out latest version available
40 | log.Printf("(hello-updater) Latest version available: %q", updater.Info.Version)
41 | }
42 |
--------------------------------------------------------------------------------
/example/run-example.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "This example will compile the hello-updater application a few times with"
4 | echo "different version strings and demonstrate go-selfupdate's functionality."
5 | echo "If the version is 'dev', no update checking will be performed."
6 | echo
7 |
8 | # build latest/dev local version of go-selfupdate
9 | SELFUPDATE_PATH=../cmd/go-selfupdate/main.go
10 | if [ -f "$SELFUPDATE_PATH" ]; then
11 | go build -o go-selfupdate ../cmd/go-selfupdate
12 | fi
13 |
14 | rm -rf deployment/update deployment/hello* public/hello-updater
15 |
16 | echo "Building example-server"
17 | go build -o example-server src/example-server/main.go
18 |
19 | echo "Running example server"
20 | killall -q example-server
21 | ./example-server &
22 |
23 | read -n 1 -p "Press any key to start." ignored; echo
24 |
25 | echo "Building dev version of hello-updater"; echo
26 | go build -ldflags="-X main.version=dev" -o hello-updater src/hello-updater/main.go
27 |
28 | echo "Creating deployment folder and copying hello-updater to it"; echo
29 | mkdir -p deployment/ && cp hello-updater deployment/
30 |
31 |
32 | echo "Running deployment/hello-updater"
33 | deployment/hello-updater
34 | read -n 1 -p "Press any key to continue." ignored; echo
35 | echo; echo "=========="; echo
36 |
37 | for (( minor=0; minor<=2; minor++ )); do
38 | echo "Building hello-updater with version set to 1.$minor"
39 | go build -ldflags="-X main.version=1.$minor" -o hello-updater src/hello-updater/main.go
40 |
41 | echo "Running ./go-selfupdate to make update available via example-server"; echo
42 | ./go-selfupdate -o public/hello-updater/ hello-updater 1.$minor
43 |
44 | if (( $minor == 0 )); then
45 | echo "Copying version 1.0 to deployment so it can self-update"; echo
46 | cp hello-updater deployment/
47 | cp hello-updater deployment/hello-updater-1.0
48 | fi
49 |
50 | echo "Running deployment/hello-updater"
51 | deployment/hello-updater
52 | read -n 1 -p "Press any key to continue." ignored; echo
53 | echo; echo "=========="; echo
54 | done
55 |
56 | echo "Running deployment/hello-updater-1.0 backup copy"
57 | deployment/hello-updater-1.0
58 | read -n 1 -p "Press any key to continue." ignored; echo
59 | echo; echo "=========="; echo
60 |
61 | echo "Building unknown version of hello-updater"; echo
62 | go build -ldflags="-X main.version=unknown" -o hello-updater src/hello-updater/main.go
63 | echo "Copying unknown version to deployment so it can self-update"; echo
64 | cp hello-updater deployment/
65 |
66 | echo "Running deployment/hello-updater"
67 | deployment/hello-updater
68 | sleep 5
69 | echo; echo "Re-running deployment/hello-updater"
70 | deployment/hello-updater
71 | sleep 5
72 | echo; echo
73 |
74 | echo "Shutting down example-server"
75 | killall example-server
76 |
--------------------------------------------------------------------------------
/cmd/go-selfupdate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "crypto/sha256"
7 | "encoding/json"
8 | "flag"
9 | "fmt"
10 | "io"
11 | "io/ioutil"
12 | "os"
13 | "path/filepath"
14 | "runtime"
15 |
16 | "github.com/kr/binarydist"
17 | )
18 |
19 | var version, genDir string
20 |
21 | type current struct {
22 | Version string
23 | Sha256 []byte
24 | }
25 |
26 | func generateSha256(path string) []byte {
27 | h := sha256.New()
28 | b, err := ioutil.ReadFile(path)
29 | if err != nil {
30 | fmt.Println(err)
31 | }
32 | h.Write(b)
33 | sum := h.Sum(nil)
34 | return sum
35 | //return base64.URLEncoding.EncodeToString(sum)
36 | }
37 |
38 | type gzReader struct {
39 | z, r io.ReadCloser
40 | }
41 |
42 | func (g *gzReader) Read(p []byte) (int, error) {
43 | return g.z.Read(p)
44 | }
45 |
46 | func (g *gzReader) Close() error {
47 | g.z.Close()
48 | return g.r.Close()
49 | }
50 |
51 | func newGzReader(r io.ReadCloser) io.ReadCloser {
52 | var err error
53 | g := new(gzReader)
54 | g.r = r
55 | g.z, err = gzip.NewReader(r)
56 | if err != nil {
57 | panic(err)
58 | }
59 | return g
60 | }
61 |
62 | func createUpdate(path string, platform string) {
63 | c := current{Version: version, Sha256: generateSha256(path)}
64 |
65 | b, err := json.MarshalIndent(c, "", " ")
66 | if err != nil {
67 | fmt.Println("error:", err)
68 | }
69 | err = ioutil.WriteFile(filepath.Join(genDir, platform+".json"), b, 0755)
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | os.MkdirAll(filepath.Join(genDir, version), 0755)
75 |
76 | var buf bytes.Buffer
77 | w := gzip.NewWriter(&buf)
78 | f, err := ioutil.ReadFile(path)
79 | if err != nil {
80 | panic(err)
81 | }
82 | w.Write(f)
83 | w.Close() // You must close this first to flush the bytes to the buffer.
84 | err = ioutil.WriteFile(filepath.Join(genDir, version, platform+".gz"), buf.Bytes(), 0755)
85 |
86 | files, err := ioutil.ReadDir(genDir)
87 | if err != nil {
88 | fmt.Println(err)
89 | }
90 |
91 | for _, file := range files {
92 | if file.IsDir() == false {
93 | continue
94 | }
95 | if file.Name() == version {
96 | continue
97 | }
98 |
99 | os.Mkdir(filepath.Join(genDir, file.Name(), version), 0755)
100 |
101 | fName := filepath.Join(genDir, file.Name(), platform+".gz")
102 | old, err := os.Open(fName)
103 | if err != nil {
104 | // Don't have an old release for this os/arch, continue on
105 | continue
106 | }
107 |
108 | fName = filepath.Join(genDir, version, platform+".gz")
109 | newF, err := os.Open(fName)
110 | if err != nil {
111 | fmt.Fprintf(os.Stderr, "Can't open %s: error: %s\n", fName, err)
112 | os.Exit(1)
113 | }
114 |
115 | ar := newGzReader(old)
116 | defer ar.Close()
117 | br := newGzReader(newF)
118 | defer br.Close()
119 | patch := new(bytes.Buffer)
120 | if err := binarydist.Diff(ar, br, patch); err != nil {
121 | panic(err)
122 | }
123 | ioutil.WriteFile(filepath.Join(genDir, file.Name(), version, platform), patch.Bytes(), 0755)
124 | }
125 | }
126 |
127 | func printUsage() {
128 | fmt.Println("")
129 | fmt.Println("Positional arguments:")
130 | fmt.Println("\tSingle platform: go-selfupdate myapp 1.2")
131 | fmt.Println("\tCross platform: go-selfupdate /tmp/mybinares/ 1.2")
132 | }
133 |
134 | func createBuildDir() {
135 | os.MkdirAll(genDir, 0755)
136 | }
137 |
138 | func main() {
139 | outputDirFlag := flag.String("o", "public", "Output directory for writing updates")
140 |
141 | var defaultPlatform string
142 | goos := os.Getenv("GOOS")
143 | goarch := os.Getenv("GOARCH")
144 | if goos != "" && goarch != "" {
145 | defaultPlatform = goos + "-" + goarch
146 | } else {
147 | defaultPlatform = runtime.GOOS + "-" + runtime.GOARCH
148 | }
149 | platformFlag := flag.String("platform", defaultPlatform,
150 | "Target platform in the form OS-ARCH. Defaults to running os/arch or the combination of the environment variables GOOS and GOARCH if both are set.")
151 |
152 | flag.Parse()
153 | if flag.NArg() < 2 {
154 | flag.Usage()
155 | printUsage()
156 | os.Exit(0)
157 | }
158 |
159 | platform := *platformFlag
160 | appPath := flag.Arg(0)
161 | version = flag.Arg(1)
162 | genDir = *outputDirFlag
163 |
164 | createBuildDir()
165 |
166 | // If dir is given create update for each file
167 | fi, err := os.Stat(appPath)
168 | if err != nil {
169 | panic(err)
170 | }
171 |
172 | if fi.IsDir() {
173 | files, err := ioutil.ReadDir(appPath)
174 | if err == nil {
175 | for _, file := range files {
176 | createUpdate(filepath.Join(appPath, file.Name()), file.Name())
177 | }
178 | os.Exit(0)
179 | }
180 | }
181 |
182 | createUpdate(appPath, platform)
183 | }
184 |
--------------------------------------------------------------------------------
/selfupdate/selfupdate_test.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestUpdaterFetchMustReturnNonNilReaderCloser(t *testing.T) {
11 | mr := &mockRequester{}
12 | mr.handleRequest(
13 | func(url string) (io.ReadCloser, error) {
14 | return nil, nil
15 | })
16 | updater := createUpdater(mr)
17 | updater.CheckTime = 24
18 | updater.RandomizeTime = 24
19 |
20 | err := updater.BackgroundRun()
21 |
22 | if err != nil {
23 | equals(t, "Fetch was expected to return non-nil ReadCloser", err.Error())
24 | } else {
25 | t.Log("Expected an error")
26 | t.Fail()
27 | }
28 | }
29 |
30 | func TestUpdaterWithEmptyPayloadNoErrorNoUpdate(t *testing.T) {
31 | mr := &mockRequester{}
32 | mr.handleRequest(
33 | func(url string) (io.ReadCloser, error) {
34 | equals(t, "http://updates.yourdomain.com/myapp/linux-amd64.json", url)
35 | return newTestReaderCloser("{}"), nil
36 | })
37 | updater := createUpdater(mr)
38 | updater.CheckTime = 24
39 | updater.RandomizeTime = 24
40 |
41 | err := updater.BackgroundRun()
42 | if err != nil {
43 | t.Errorf("Error occurred: %#v", err)
44 | }
45 | }
46 |
47 | func TestUpdaterCheckTime(t *testing.T) {
48 | mr := &mockRequester{}
49 | mr.handleRequest(
50 | func(url string) (io.ReadCloser, error) {
51 | equals(t, "http://updates.yourdomain.com/myapp/linux-amd64.json", url)
52 | return newTestReaderCloser("{}"), nil
53 | })
54 |
55 | // Run test with various time
56 | runTestTimeChecks(t, mr, 0, 0, false)
57 | runTestTimeChecks(t, mr, 0, 5, true)
58 | runTestTimeChecks(t, mr, 1, 0, true)
59 | runTestTimeChecks(t, mr, 100, 100, true)
60 | }
61 |
62 | // Helper function to run check time tests
63 | func runTestTimeChecks(t *testing.T, mr *mockRequester, checkTime int, randomizeTime int, expectUpdate bool) {
64 | updater := createUpdater(mr)
65 | updater.ClearUpdateState()
66 | updater.CheckTime = checkTime
67 | updater.RandomizeTime = randomizeTime
68 |
69 | updater.BackgroundRun()
70 |
71 | if updater.WantUpdate() == expectUpdate {
72 | t.Errorf("WantUpdate returned %v; want %v", updater.WantUpdate(), expectUpdate)
73 | }
74 |
75 | maxHrs := time.Duration(updater.CheckTime+updater.RandomizeTime) * time.Hour
76 | maxTime := time.Now().Add(maxHrs)
77 |
78 | if !updater.NextUpdate().Before(maxTime) {
79 | t.Errorf("NextUpdate should less than %s hrs (CheckTime + RandomizeTime) from now; now %s; next update %s", maxHrs, time.Now(), updater.NextUpdate())
80 | }
81 |
82 | if maxHrs > 0 && !updater.NextUpdate().After(time.Now()) {
83 | t.Errorf("NextUpdate should be after now")
84 | }
85 | }
86 |
87 | func TestUpdaterWithEmptyPayloadNoErrorNoUpdateEscapedPath(t *testing.T) {
88 | mr := &mockRequester{}
89 | mr.handleRequest(
90 | func(url string) (io.ReadCloser, error) {
91 | equals(t, "http://updates.yourdomain.com/myapp%2Bfoo/darwin-amd64.json", url)
92 | return newTestReaderCloser("{}"), nil
93 | })
94 | updater := createUpdaterWithEscapedCharacters(mr)
95 |
96 | err := updater.BackgroundRun()
97 | if err != nil {
98 | t.Errorf("Error occurred: %#v", err)
99 | }
100 | }
101 |
102 | func TestUpdateAvailable(t *testing.T) {
103 | mr := &mockRequester{}
104 | mr.handleRequest(
105 | func(url string) (io.ReadCloser, error) {
106 | equals(t, "http://updates.yourdomain.com/myapp/linux-amd64.json", url)
107 | return newTestReaderCloser(`{
108 | "Version": "2023-07-09-66c6c12",
109 | "Sha256": "Q2vvTOW0p69A37StVANN+/ko1ZQDTElomq7fVcex/02="
110 | }`), nil
111 | })
112 | updater := createUpdater(mr)
113 |
114 | version, err := updater.UpdateAvailable()
115 | if err != nil {
116 | t.Errorf("Error occurred: %#v", err)
117 | }
118 | equals(t, "2023-07-09-66c6c12", version)
119 | }
120 |
121 | func createUpdater(mr *mockRequester) *Updater {
122 | return &Updater{
123 | CurrentVersion: "1.2",
124 | ApiURL: "http://updates.yourdomain.com/",
125 | BinURL: "http://updates.yourdownmain.com/",
126 | DiffURL: "http://updates.yourdomain.com/",
127 | Dir: "update/",
128 | CmdName: "myapp", // app name
129 | Requester: mr,
130 | }
131 | }
132 |
133 | func createUpdaterWithEscapedCharacters(mr *mockRequester) *Updater {
134 | return &Updater{
135 | CurrentVersion: "1.2+foobar",
136 | ApiURL: "http://updates.yourdomain.com/",
137 | BinURL: "http://updates.yourdownmain.com/",
138 | DiffURL: "http://updates.yourdomain.com/",
139 | Dir: "update/",
140 | CmdName: "myapp+foo", // app name
141 | Requester: mr,
142 | }
143 | }
144 |
145 | func equals(t *testing.T, expected, actual interface{}) {
146 | if expected != actual {
147 | t.Logf("Expected: %#v got %#v\n", expected, actual)
148 | t.Fail()
149 | }
150 | }
151 |
152 | type testReadCloser struct {
153 | buffer *bytes.Buffer
154 | }
155 |
156 | func newTestReaderCloser(payload string) io.ReadCloser {
157 | return &testReadCloser{buffer: bytes.NewBufferString(payload)}
158 | }
159 |
160 | func (trc *testReadCloser) Read(p []byte) (n int, err error) {
161 | return trc.buffer.Read(p)
162 | }
163 |
164 | func (trc *testReadCloser) Close() error {
165 | return nil
166 | }
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-selfupdate
2 |
3 | [](https://godoc.org/github.com/sanbornm/go-selfupdate/selfupdate)
4 | 
5 |
6 | Enable your Golang applications to self update. Inspired by Chrome based on Heroku's [hk](https://github.com/heroku/hk).
7 |
8 | ## Features
9 |
10 | * Tested on Mac, Linux, Arm, and Windows
11 | * Creates binary diffs with [bsdiff](http://www.daemonology.net/bsdiff/) allowing small incremental updates
12 | * Falls back to full binary update if diff fails to match SHA
13 |
14 | ## QuickStart
15 |
16 | ### Install library and update/patch creation utility
17 |
18 | `go install github.com/sanbornm/go-selfupdate/cmd/go-selfupdate@latest`
19 |
20 | ### Enable your App to Self Update
21 |
22 | `go get -u github.com/sanbornm/go-selfupdate/...`
23 |
24 | var updater = &selfupdate.Updater{
25 | CurrentVersion: version, // the current version of your app used to determine if an update is necessary
26 | // these endpoints can be the same if everything is hosted in the same place
27 | ApiURL: "http://updates.yourdomain.com/", // endpoint to get update manifest
28 | BinURL: "http://updates.yourdomain.com/", // endpoint to get full binaries
29 | DiffURL: "http://updates.yourdomain.com/", // endpoint to get binary diff/patches
30 | Dir: "update/", // directory relative to your app to store temporary state files related to go-selfupdate
31 | CmdName: "myapp", // your app's name (must correspond to app name hosting the updates)
32 | // app name allows you to serve updates for multiple apps on the same server/endpoint
33 | }
34 |
35 | // go look for an update when your app starts up
36 | go updater.BackgroundRun()
37 | // your app continues to run...
38 |
39 | ### Push Out and Update
40 |
41 | go-selfupdate path-to-your-app the-version
42 | go-selfupdate myapp 1.2
43 |
44 | By default this will create a folder in your project called *public*. You can then rsync or transfer this to your webserver or S3. To change the output directory use `-o` flag.
45 |
46 | If you are cross compiling you can specify a directory:
47 |
48 | go-selfupdate /tmp/mybinares/ 1.2
49 |
50 | The directory should contain files with the name, $GOOS-$ARCH. Example:
51 |
52 | windows-386
53 | darwin-amd64
54 | linux-arm
55 |
56 | If you are using [goxc](https://github.com/laher/goxc) you can output the files with this naming format by specifying this config:
57 |
58 | "OutPath": "{{.Dest}}{{.PS}}{{.Version}}{{.PS}}{{.Os}}-{{.Arch}}",
59 |
60 | ## Update Protocol
61 |
62 | Updates are fetched from an HTTP(s) server. AWS S3 or static hosting can be used. A JSON manifest file is pulled first which points to the wanted version (usually latest) and matching metadata. SHA256 hash is currently the only metadata but new fields may be added here like signatures. `go-selfupdate` isn't aware of any versioning schemes. It doesn't know major/minor versions. It just knows the target version by name and can apply diffs based on current version and version you wish to move to. For example 1.0 to 5.0 or 1.0 to 1.1. You don't even need to use point numbers. You can use hashes, dates, etc for versions.
63 |
64 | GET yourserver.com/appname/linux-amd64.json
65 |
66 | 200 ok
67 | {
68 | "Version": "2",
69 | "Sha256": "..." // base64
70 | }
71 |
72 | then
73 |
74 | GET patches.yourserver.com/appname/1.1/1.2/linux-amd64
75 |
76 | 200 ok
77 | [bsdiff data]
78 |
79 | or
80 |
81 | GET fullbins.yourserver.com/appname/1.0/linux-amd64.gz
82 |
83 | 200 ok
84 | [gzipped executable data]
85 |
86 | The only required files are `/-.json` and `//-.gz` everything else is optional. If you wanted to you could skip using go-selfupdate CLI tool and generate these two files manually or with another tool.
87 |
88 | ## Config
89 |
90 | Updater Config options:
91 |
92 | type Updater struct {
93 | CurrentVersion string // Currently running version. `dev` is a special version here and will cause the updater to never update.
94 | ApiURL string // Base URL for API requests (JSON files).
95 | CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
96 | BinURL string // Base URL for full binary downloads.
97 | DiffURL string // Base URL for diff downloads.
98 | Dir string // Directory to store selfupdate state.
99 | ForceCheck bool // Check for update regardless of cktime timestamp
100 | CheckTime int // Time in hours before next check
101 | RandomizeTime int // Time in hours to randomize with CheckTime
102 | Requester Requester // Optional parameter to override existing HTTP request handler
103 | Info struct {
104 | Version string
105 | Sha256 []byte
106 | }
107 | OnSuccessfulUpdate func() // Optional function to run after an update has successfully taken place
108 | }
109 |
110 | ### Restart on update
111 |
112 | It is common for an app to want to restart to apply the update. `go-selfupdate` gives you a hook to do that but leaves it up to you on how and when to restart as it differs for all apps. If you have a service restart application like Docker or systemd you can simply exit and let the upstream app start/restart your application. Just set the `OnSuccessfulUpdate` hook:
113 |
114 | u.OnSuccessfulUpdate = func() { os.Exit(0) }
115 |
116 | Or maybe you have a fancy graceful restart library/func:
117 |
118 | u.OnSuccessfulUpdate = func() { gracefullyRestartMyApp() }
119 |
120 | ## State
121 |
122 | go-selfupdate will keep a Go time.Time formatted timestamp in a file named `cktime` in folder specified by `Updater.Dir`. This can be useful for debugging to see when the next update can be applied or allow other applications to manipulate it.
--------------------------------------------------------------------------------
/selfupdate/selfupdate.go:
--------------------------------------------------------------------------------
1 | package selfupdate
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "crypto/sha256"
7 | "encoding/json"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "math/rand"
14 | "net/url"
15 | "os"
16 | "path/filepath"
17 | "runtime"
18 | "time"
19 |
20 | "github.com/kr/binarydist"
21 | )
22 |
23 | const (
24 | // holds a timestamp which triggers the next update
25 | upcktimePath = "cktime" // path to timestamp file relative to u.Dir
26 | plat = runtime.GOOS + "-" + runtime.GOARCH // ex: linux-amd64
27 | )
28 |
29 | var (
30 | ErrHashMismatch = errors.New("new file hash mismatch after patch")
31 |
32 | defaultHTTPRequester = HTTPRequester{}
33 | )
34 |
35 | // Updater is the configuration and runtime data for doing an update.
36 | //
37 | // Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location.
38 | //
39 | // Example:
40 | //
41 | // updater := &selfupdate.Updater{
42 | // CurrentVersion: version,
43 | // ApiURL: "http://updates.yourdomain.com/",
44 | // BinURL: "http://updates.yourdownmain.com/",
45 | // DiffURL: "http://updates.yourdomain.com/",
46 | // Dir: "update/",
47 | // CmdName: "myapp", // app name
48 | // }
49 | // if updater != nil {
50 | // go updater.BackgroundRun()
51 | // }
52 | type Updater struct {
53 | CurrentVersion string // Currently running version. `dev` is a special version here and will cause the updater to never update.
54 | ApiURL string // Base URL for API requests (JSON files).
55 | CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary.
56 | BinURL string // Base URL for full binary downloads.
57 | DiffURL string // Base URL for diff downloads.
58 | Dir string // Directory to store selfupdate state.
59 | ForceCheck bool // Check for update regardless of cktime timestamp
60 | CheckTime int // Time in hours before next check
61 | RandomizeTime int // Time in hours to randomize with CheckTime
62 | Requester Requester // Optional parameter to override existing HTTP request handler
63 | Info struct {
64 | Version string
65 | Sha256 []byte
66 | }
67 | OnSuccessfulUpdate func() // Optional function to run after an update has successfully taken place
68 | }
69 |
70 | func (u *Updater) getExecRelativeDir(dir string) string {
71 | filename, _ := os.Executable()
72 | path := filepath.Join(filepath.Dir(filename), dir)
73 | return path
74 | }
75 |
76 | func canUpdate() (err error) {
77 | // get the directory the file exists in
78 | path, err := os.Executable()
79 | if err != nil {
80 | return
81 | }
82 |
83 | fileDir := filepath.Dir(path)
84 | fileName := filepath.Base(path)
85 |
86 | // attempt to open a file in the file's directory
87 | newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName))
88 | fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
89 | if err != nil {
90 | return
91 | }
92 | fp.Close()
93 |
94 | _ = os.Remove(newPath)
95 | return
96 | }
97 |
98 | // BackgroundRun starts the update check and apply cycle.
99 | func (u *Updater) BackgroundRun() error {
100 | if err := os.MkdirAll(u.getExecRelativeDir(u.Dir), 0755); err != nil {
101 | // fail
102 | return err
103 | }
104 | // check to see if we want to check for updates based on version
105 | // and last update time
106 | if u.WantUpdate() {
107 | if err := canUpdate(); err != nil {
108 | // fail
109 | return err
110 | }
111 |
112 | u.SetUpdateTime()
113 |
114 | if err := u.Update(); err != nil {
115 | return err
116 | }
117 | }
118 | return nil
119 | }
120 |
121 | // WantUpdate returns boolean designating if an update is desired. If the app's version
122 | // is `dev` WantUpdate will return false. If u.ForceCheck is true or cktime is after now
123 | // WantUpdate will return true.
124 | func (u *Updater) WantUpdate() bool {
125 | if u.CurrentVersion == "dev" || (!u.ForceCheck && u.NextUpdate().After(time.Now())) {
126 | return false
127 | }
128 |
129 | return true
130 | }
131 |
132 | // NextUpdate returns the next time update should be checked
133 | func (u *Updater) NextUpdate() time.Time {
134 | path := u.getExecRelativeDir(u.Dir + upcktimePath)
135 | nextTime := readTime(path)
136 |
137 | return nextTime
138 | }
139 |
140 | // SetUpdateTime writes the next update time to the state file
141 | func (u *Updater) SetUpdateTime() bool {
142 | path := u.getExecRelativeDir(u.Dir + upcktimePath)
143 | wait := time.Duration(u.CheckTime) * time.Hour
144 | // Add 1 to random time since max is not included
145 | waitrand := time.Duration(rand.Intn(u.RandomizeTime+1)) * time.Hour
146 |
147 | return writeTime(path, time.Now().Add(wait+waitrand))
148 | }
149 |
150 | // ClearUpdateState writes current time to state file
151 | func (u *Updater) ClearUpdateState() {
152 | path := u.getExecRelativeDir(u.Dir + upcktimePath)
153 | os.Remove(path)
154 | }
155 |
156 | // UpdateAvailable checks if update is available and returns version
157 | func (u *Updater) UpdateAvailable() (string, error) {
158 | path, err := os.Executable()
159 | if err != nil {
160 | return "", err
161 | }
162 | old, err := os.Open(path)
163 | if err != nil {
164 | return "", err
165 | }
166 | defer old.Close()
167 |
168 | err = u.fetchInfo()
169 | if err != nil {
170 | return "", err
171 | }
172 | if u.Info.Version == u.CurrentVersion {
173 | return "", nil
174 | } else {
175 | return u.Info.Version, nil
176 | }
177 | }
178 |
179 | // Update initiates the self update process
180 | func (u *Updater) Update() error {
181 | path, err := os.Executable()
182 | if err != nil {
183 | return err
184 | }
185 |
186 | if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
187 | path = resolvedPath
188 | }
189 |
190 | // go fetch latest updates manifest
191 | err = u.fetchInfo()
192 | if err != nil {
193 | return err
194 | }
195 |
196 | // we are on the latest version, nothing to do
197 | if u.Info.Version == u.CurrentVersion {
198 | return nil
199 | }
200 |
201 | old, err := os.Open(path)
202 | if err != nil {
203 | return err
204 | }
205 | defer old.Close()
206 |
207 | bin, err := u.fetchAndVerifyPatch(old)
208 | if err != nil {
209 | if err == ErrHashMismatch {
210 | log.Println("update: hash mismatch from patched binary")
211 | } else {
212 | if u.DiffURL != "" {
213 | log.Println("update: patching binary,", err)
214 | }
215 | }
216 |
217 | // if patch failed grab the full new bin
218 | bin, err = u.fetchAndVerifyFullBin()
219 | if err != nil {
220 | if err == ErrHashMismatch {
221 | log.Println("update: hash mismatch from full binary")
222 | } else {
223 | log.Println("update: fetching full binary,", err)
224 | }
225 | return err
226 | }
227 | }
228 |
229 | // close the old binary before installing because on windows
230 | // it can't be renamed if a handle to the file is still open
231 | old.Close()
232 |
233 | err, errRecover := fromStream(bytes.NewBuffer(bin))
234 | if errRecover != nil {
235 | return fmt.Errorf("update and recovery errors: %q %q", err, errRecover)
236 | }
237 | if err != nil {
238 | return err
239 | }
240 |
241 | // update was successful, run func if set
242 | if u.OnSuccessfulUpdate != nil {
243 | u.OnSuccessfulUpdate()
244 | }
245 |
246 | return nil
247 | }
248 |
249 | func fromStream(updateWith io.Reader) (err error, errRecover error) {
250 | updatePath, err := os.Executable()
251 | if err != nil {
252 | return
253 | }
254 |
255 | var newBytes []byte
256 | newBytes, err = ioutil.ReadAll(updateWith)
257 | if err != nil {
258 | return
259 | }
260 |
261 | // get the directory the executable exists in
262 | updateDir := filepath.Dir(updatePath)
263 | filename := filepath.Base(updatePath)
264 |
265 | // Copy the contents of of newbinary to a the new executable file
266 | newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
267 | fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
268 | if err != nil {
269 | return
270 | }
271 | defer fp.Close()
272 | _, err = io.Copy(fp, bytes.NewReader(newBytes))
273 |
274 | // if we don't call fp.Close(), windows won't let us move the new executable
275 | // because the file will still be "in use"
276 | fp.Close()
277 |
278 | // this is where we'll move the executable to so that we can swap in the updated replacement
279 | oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
280 |
281 | // delete any existing old exec file - this is necessary on Windows for two reasons:
282 | // 1. after a successful update, Windows can't remove the .old file because the process is still running
283 | // 2. windows rename operations fail if the destination file already exists
284 | _ = os.Remove(oldPath)
285 |
286 | // move the existing executable to a new file in the same directory
287 | err = os.Rename(updatePath, oldPath)
288 | if err != nil {
289 | return
290 | }
291 |
292 | // move the new exectuable in to become the new program
293 | err = os.Rename(newPath, updatePath)
294 |
295 | if err != nil {
296 | // copy unsuccessful
297 | errRecover = os.Rename(oldPath, updatePath)
298 | } else {
299 | // copy successful, remove the old binary
300 | errRemove := os.Remove(oldPath)
301 |
302 | // windows has trouble with removing old binaries, so hide it instead
303 | if errRemove != nil {
304 | _ = hideFile(oldPath)
305 | }
306 | }
307 |
308 | return
309 | }
310 |
311 | // fetchInfo fetches the update JSON manifest at u.ApiURL/appname/platform.json
312 | // and updates u.Info.
313 | func (u *Updater) fetchInfo() error {
314 | r, err := u.fetch(u.ApiURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(plat) + ".json")
315 | if err != nil {
316 | return err
317 | }
318 | defer r.Close()
319 | err = json.NewDecoder(r).Decode(&u.Info)
320 | if err != nil {
321 | return err
322 | }
323 | if len(u.Info.Sha256) != sha256.Size {
324 | return errors.New("bad cmd hash in info")
325 | }
326 | return nil
327 | }
328 |
329 | func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) {
330 | bin, err := u.fetchAndApplyPatch(old)
331 | if err != nil {
332 | return nil, err
333 | }
334 | if !verifySha(bin, u.Info.Sha256) {
335 | return nil, ErrHashMismatch
336 | }
337 | return bin, nil
338 | }
339 |
340 | func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) {
341 | r, err := u.fetch(u.DiffURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(u.CurrentVersion) + "/" + url.QueryEscape(u.Info.Version) + "/" + url.QueryEscape(plat))
342 | if err != nil {
343 | return nil, err
344 | }
345 | defer r.Close()
346 | var buf bytes.Buffer
347 | err = binarydist.Patch(old, &buf, r)
348 | return buf.Bytes(), err
349 | }
350 |
351 | func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) {
352 | bin, err := u.fetchBin()
353 | if err != nil {
354 | return nil, err
355 | }
356 | verified := verifySha(bin, u.Info.Sha256)
357 | if !verified {
358 | return nil, ErrHashMismatch
359 | }
360 | return bin, nil
361 | }
362 |
363 | func (u *Updater) fetchBin() ([]byte, error) {
364 | r, err := u.fetch(u.BinURL + url.QueryEscape(u.CmdName) + "/" + url.QueryEscape(u.Info.Version) + "/" + url.QueryEscape(plat) + ".gz")
365 | if err != nil {
366 | return nil, err
367 | }
368 | defer r.Close()
369 | buf := new(bytes.Buffer)
370 | gz, err := gzip.NewReader(r)
371 | if err != nil {
372 | return nil, err
373 | }
374 | if _, err = io.Copy(buf, gz); err != nil {
375 | return nil, err
376 | }
377 |
378 | return buf.Bytes(), nil
379 | }
380 |
381 | func (u *Updater) fetch(url string) (io.ReadCloser, error) {
382 | if u.Requester == nil {
383 | return defaultHTTPRequester.Fetch(url)
384 | }
385 |
386 | readCloser, err := u.Requester.Fetch(url)
387 | if err != nil {
388 | return nil, err
389 | }
390 |
391 | if readCloser == nil {
392 | return nil, fmt.Errorf("Fetch was expected to return non-nil ReadCloser")
393 | }
394 |
395 | return readCloser, nil
396 | }
397 |
398 | func readTime(path string) time.Time {
399 | p, err := ioutil.ReadFile(path)
400 | if os.IsNotExist(err) {
401 | return time.Time{}
402 | }
403 | if err != nil {
404 | return time.Now().Add(1000 * time.Hour)
405 | }
406 | t, err := time.Parse(time.RFC3339, string(p))
407 | if err != nil {
408 | return time.Now().Add(1000 * time.Hour)
409 | }
410 | return t
411 | }
412 |
413 | func verifySha(bin []byte, sha []byte) bool {
414 | h := sha256.New()
415 | h.Write(bin)
416 | return bytes.Equal(h.Sum(nil), sha)
417 | }
418 |
419 | func writeTime(path string, t time.Time) bool {
420 | return ioutil.WriteFile(path, []byte(t.Format(time.RFC3339)), 0644) == nil
421 | }
422 |
--------------------------------------------------------------------------------