mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
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:
184
version/update.go
Normal file
184
version/update.go
Normal 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
101
version/update_test.go
Normal 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
5
version/url.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package version
|
||||
|
||||
const (
|
||||
downloadURL = "https://app.netbird.io/install"
|
||||
)
|
||||
33
version/url_darwin.go
Normal file
33
version/url_darwin.go
Normal 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
6
version/url_linux.go
Normal 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
19
version/url_windows.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user