netbird/version/update.go
Zoltan Papp 6d4240a5ae
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.
2023-10-30 10:32:48 +01:00

185 lines
3.7 KiB
Go

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
}