Feature/update check (#1232)

Periodically fetch the latest available version, and the UI will shows a new menu for the download link. It checks both the daemon version and the UI version.
This commit is contained in:
Zoltan Papp
2023-10-30 10:32:48 +01:00
committed by GitHub
parent 52f5101715
commit 6d4240a5ae
23 changed files with 492 additions and 18 deletions

184
version/update.go Normal file
View File

@@ -0,0 +1,184 @@
package version
import (
"io"
"net/http"
"sync"
"time"
goversion "github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus"
)
const (
fetchPeriod = 30 * time.Minute
)
var (
versionURL = "https://pkgs.netbird.io/releases/latest/version"
)
// Update fetch the version info periodically and notify the onUpdateListener in case the UI version or the
// daemon version are deprecated
type Update struct {
uiVersion *goversion.Version
daemonVersion *goversion.Version
latestAvailable *goversion.Version
versionsLock sync.Mutex
fetchTicker *time.Ticker
fetchDone chan struct{}
onUpdateListener func()
listenerLock sync.Mutex
}
// NewUpdate instantiate Update and start to fetch the new version information
func NewUpdate() *Update {
currentVersion, err := goversion.NewVersion(version)
if err != nil {
currentVersion, _ = goversion.NewVersion("0.0.0")
}
latestAvailable, _ := goversion.NewVersion("0.0.0")
u := &Update{
latestAvailable: latestAvailable,
uiVersion: currentVersion,
fetchTicker: time.NewTicker(fetchPeriod),
fetchDone: make(chan struct{}),
}
go u.startFetcher()
return u
}
// StopWatch stop the version info fetch loop
func (u *Update) StopWatch() {
u.fetchTicker.Stop()
select {
case u.fetchDone <- struct{}{}:
default:
}
}
// SetDaemonVersion update the currently running daemon version. If new version is available it will trigger
// the onUpdateListener
func (u *Update) SetDaemonVersion(newVersion string) bool {
daemonVersion, err := goversion.NewVersion(newVersion)
if err != nil {
daemonVersion, _ = goversion.NewVersion("0.0.0")
}
u.versionsLock.Lock()
if u.daemonVersion != nil && u.daemonVersion.Equal(daemonVersion) {
u.versionsLock.Unlock()
return false
}
u.daemonVersion = daemonVersion
u.versionsLock.Unlock()
return u.checkUpdate()
}
// SetOnUpdateListener set new update listener
func (u *Update) SetOnUpdateListener(updateFn func()) {
u.listenerLock.Lock()
defer u.listenerLock.Unlock()
u.onUpdateListener = updateFn
if u.isUpdateAvailable() {
u.onUpdateListener()
}
}
func (u *Update) startFetcher() {
changed := u.fetchVersion()
if changed {
u.checkUpdate()
}
select {
case <-u.fetchDone:
return
case <-u.fetchTicker.C:
changed := u.fetchVersion()
if changed {
u.checkUpdate()
}
}
}
func (u *Update) fetchVersion() bool {
resp, err := http.Get(versionURL)
if err != nil {
log.Errorf("failed to fetch version info: %s", err)
return false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Errorf("invalid status code: %d", resp.StatusCode)
return false
}
if resp.ContentLength > 100 {
log.Errorf("too large response: %d", resp.ContentLength)
return false
}
content, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf("failed to read content: %s", err)
return false
}
latestAvailable, err := goversion.NewVersion(string(content))
if err != nil {
log.Errorf("failed to parse the version string: %s", err)
return false
}
u.versionsLock.Lock()
defer u.versionsLock.Unlock()
if u.latestAvailable.Equal(latestAvailable) {
return false
}
u.latestAvailable = latestAvailable
return true
}
func (u *Update) checkUpdate() bool {
if !u.isUpdateAvailable() {
return false
}
u.listenerLock.Lock()
defer u.listenerLock.Unlock()
if u.onUpdateListener == nil {
return true
}
go u.onUpdateListener()
return true
}
func (u *Update) isUpdateAvailable() bool {
u.versionsLock.Lock()
defer u.versionsLock.Unlock()
if u.latestAvailable.GreaterThan(u.uiVersion) {
return true
}
if u.daemonVersion == nil {
return false
}
if u.latestAvailable.GreaterThan(u.daemonVersion) {
return true
}
return false
}

101
version/update_test.go Normal file
View File

@@ -0,0 +1,101 @@
package version
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
func TestNewUpdate(t *testing.T) {
version = "1.0.0"
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "10.0.0")
}))
defer svr.Close()
versionURL = svr.URL
wg := &sync.WaitGroup{}
wg.Add(1)
onUpdate := false
u := NewUpdate()
defer u.StopWatch()
u.SetOnUpdateListener(func() {
onUpdate = true
wg.Done()
})
waitTimeout(wg)
if onUpdate != true {
t.Errorf("update not found")
}
}
func TestDoNotUpdate(t *testing.T) {
version = "11.0.0"
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "10.0.0")
}))
defer svr.Close()
versionURL = svr.URL
wg := &sync.WaitGroup{}
wg.Add(1)
onUpdate := false
u := NewUpdate()
defer u.StopWatch()
u.SetOnUpdateListener(func() {
onUpdate = true
wg.Done()
})
waitTimeout(wg)
if onUpdate == true {
t.Errorf("invalid update")
}
}
func TestDaemonUpdate(t *testing.T) {
version = "11.0.0"
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "11.0.0")
}))
defer svr.Close()
versionURL = svr.URL
wg := &sync.WaitGroup{}
wg.Add(1)
onUpdate := false
u := NewUpdate()
defer u.StopWatch()
u.SetOnUpdateListener(func() {
onUpdate = true
wg.Done()
})
u.SetDaemonVersion("10.0.0")
waitTimeout(wg)
if onUpdate != true {
t.Errorf("invalid dameon version check")
}
}
func waitTimeout(wg *sync.WaitGroup) {
c := make(chan struct{})
go func() {
wg.Wait()
close(c)
}()
select {
case <-c:
return
case <-time.After(time.Second):
return
}
}

5
version/url.go Normal file
View File

@@ -0,0 +1,5 @@
package version
const (
downloadURL = "https://app.netbird.io/install"
)

33
version/url_darwin.go Normal file
View File

@@ -0,0 +1,33 @@
package version
import (
"os/exec"
"runtime"
)
const (
urlMacIntel = "https://pkgs.netbird.io/macos/amd64"
urlMacM1M2 = "https://pkgs.netbird.io/macos/arm64"
)
// DownloadUrl return with the proper download link
func DownloadUrl() string {
cmd := exec.Command("brew", "list --formula | grep -i netbird")
if err := cmd.Start(); err != nil {
goto PKGINSTALL
}
if err := cmd.Wait(); err == nil {
return downloadURL
}
PKGINSTALL:
switch runtime.GOARCH {
case "amd64":
return urlMacIntel
case "arm64":
return urlMacM1M2
default:
return downloadURL
}
}

6
version/url_linux.go Normal file
View File

@@ -0,0 +1,6 @@
package version
// DownloadUrl return with the proper download link
func DownloadUrl() string {
return downloadURL
}

19
version/url_windows.go Normal file
View File

@@ -0,0 +1,19 @@
package version
import "golang.org/x/sys/windows/registry"
const (
urlWinExe = "https://pkgs.netbird.io/windows/x64"
)
var regKeyAppPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Netbird"
// DownloadUrl return with the proper download link
func DownloadUrl() string {
_, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyAppPath, registry.QUERY_VALUE)
if err == nil {
return urlWinExe
} else {
return downloadURL
}
}