Skip to content

Commit

Permalink
Merge pull request #142 from denopink/denopink/check-for-latest-version
Browse files Browse the repository at this point in the history
Check for latest version of itself
  • Loading branch information
cohix authored Dec 8, 2021
2 parents ad5be13 + d58d1b1 commit c556ae3
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 2 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ target
.scntest

# macOS
.DS_Store
.DS_Store

# visual code
.vscode/
*.code-workspace
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module github.com/suborbital/subo
go 1.17

require (
github.com/google/go-github/v41 v41.0.0
github.com/hashicorp/go-version v1.3.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.2.1
github.com/suborbital/atmo v0.4.0
Expand All @@ -15,6 +17,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-redis/redis/v8 v8.11.4 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
Expand Down Expand Up @@ -206,6 +210,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand Down
37 changes: 36 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
package main

import (
"context"
"os"
"time"

"github.com/suborbital/subo/subo/release"
"github.com/suborbital/subo/subo/util"
)

const checkVersionTimeout = 500 * time.Millisecond

func main() {
rootCmd := rootCommand()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := checkVersion(ctx)

rootCmd := rootCommand()
if err := rootCmd.Execute(); err != nil {
os.Exit(-1)
}

select {
case <-done:
case <-time.After(checkVersionTimeout):
util.LogFail("failed to CheckForLatestVersion due to timeout")
}
}

func checkVersion(ctx context.Context) chan bool {
done := make(chan bool)

go func() {
if version, err := release.CheckForLatestVersion(); err != nil {
util.LogFail(err.Error())
} else if version != "" {
util.LogInfo(version)
}
select {
case <-ctx.Done():
default:
done <- true
}
}()

return done
}
152 changes: 152 additions & 0 deletions subo/release/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package release

import (
"bytes"
"context"
"encoding/gob"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"

"github.com/google/go-github/v41/github"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/suborbital/subo/subo/util"
)

const lastCheckedFilename = "subo_last_checked"
const latestReleaseFilename = "subo_latest_release"

func getTimestampCache() (time.Time, error) {
cachePath, err := util.CacheDir()
if err != nil {
return time.Time{}, errors.Wrap(err, "failed to CacheDir")
}

cachedTimestamp := time.Time{}
filePath := filepath.Join(cachePath, lastCheckedFilename)
if _, err = os.Stat(filePath); os.IsNotExist(err) {
} else if err != nil {
return time.Time{}, errors.Wrap(err, "failed to Stat")
} else {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return time.Time{}, errors.Wrap(err, "failed to ReadFile")
}

cachedTimestamp, err = time.Parse(time.RFC3339, string(data))
if err != nil {
return time.Time{}, errors.Wrap(err, "failed to parse cached timestamp")
}
}
return cachedTimestamp, nil
}

func cacheTimestamp(timestamp time.Time) error {
cachePath, err := util.CacheDir()
if err != nil {
return errors.Wrap(err, "failed to CacheDir")
}

filePath := filepath.Join(cachePath, lastCheckedFilename)
data := []byte(timestamp.Format(time.RFC3339))
if err := ioutil.WriteFile(filePath, data, os.ModePerm); err != nil {
return errors.Wrap(err, "failed to WriteFile")
}

return nil
}

func getLatestReleaseCache() (*github.RepositoryRelease, error) {
if cachedTimestamp, err := getTimestampCache(); err != nil {
return nil, errors.Wrap(err, "failed to getTimestampCache")
} else if currentTimestamp := time.Now().UTC(); cachedTimestamp.IsZero() || currentTimestamp.After(cachedTimestamp.Add(time.Hour)) {
// check if 1 hour has passed since the last version check, and update the cached timestamp and latest release if so
if err := cacheTimestamp(currentTimestamp); err != nil {
return nil, errors.Wrap(err, "failed to cacheTimestamp")
}

return nil, nil
}

cachePath, err := util.CacheDir()
if err != nil {
return nil, errors.Wrap(err, "failed to CacheDir")
}

var latestRepoRelease *github.RepositoryRelease
filepath := filepath.Join(cachePath, latestReleaseFilename)
if _, err = os.Stat(filepath); os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, errors.Wrap(err, "faild to Stat")
} else {
data, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "failed to ReadFile")
}

buffer := bytes.Buffer{}
buffer.Write(data)
decoder := gob.NewDecoder(&buffer)
err = decoder.Decode(&latestRepoRelease)
if err != nil {
return nil, errors.Wrap(err, "failed to Decode cached RepositoryRelease")
}
}

return latestRepoRelease, nil
}

func cacheLatestRelease(latestRepoRelease *github.RepositoryRelease) error {
cachePath, err := util.CacheDir()
if err != nil {
return errors.Wrap(err, "failed to CacheDir")
}

buffer := bytes.Buffer{}
encoder := gob.NewEncoder(&buffer)
if err = encoder.Encode(latestRepoRelease); err != nil {
return errors.Wrap(err, "failed to Encode RepositoryRelease")
} else if err := ioutil.WriteFile(filepath.Join(cachePath, latestReleaseFilename), buffer.Bytes(), os.ModePerm); err != nil {
return errors.Wrap(err, "failed to WriteFile")
}

return nil
}

func getLatestVersion() (*version.Version, error) {
latestRepoRelease, err := getLatestReleaseCache()
if err != nil {
return nil, errors.Wrap(err, "failed to getTimestampCache")
} else if latestRepoRelease == nil {
latestRepoRelease, _, err = github.NewClient(nil).Repositories.GetLatestRelease(context.Background(), "suborbital", "subo")
if err != nil {
return nil, errors.Wrap(err, "failed to fetch latest subo release")
} else if err = cacheLatestRelease(latestRepoRelease); err != nil {
return nil, errors.Wrap(err, "failed to cacheLatestRelease")
}
}

latestVersion, err := version.NewVersion(*latestRepoRelease.TagName)
if err != nil {
return nil, errors.Wrap(err, "failed to parse latest subo version")
}

return latestVersion, nil
}

// CheckForLatestVersion returns an error if SuboDotVersion does not match the latest GitHub release or if the check fails
func CheckForLatestVersion() (string, error) {
if latestCmdVersion, err := getLatestVersion(); err != nil {
return "", errors.Wrap(err, "failed to getLatestVersion")
} else if cmdVersion, err := version.NewVersion(SuboDotVersion); err != nil {
return "", errors.Wrap(err, "failed to parse current subo version")
} else if cmdVersion.LessThan(latestCmdVersion) {
return fmt.Sprintf("An upgrade for subo is available: %s → %s\n", cmdVersion, latestCmdVersion), nil
}

return "", nil
}
20 changes: 20 additions & 0 deletions subo/util/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package util

import (
"os"
"path/filepath"

"github.com/pkg/errors"
)

// CacheDir returns the cache directory and creates it if it doesn't exist
func CacheDir() (string, error) {
targetPath := filepath.Join(os.TempDir(), "suborbital", "subo")

if _, err := os.Stat(targetPath); os.IsNotExist(err) {
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
return "", errors.Wrap(err, "failed to MkdirAll")
}
}
return targetPath, nil
}

0 comments on commit c556ae3

Please sign in to comment.