From 27d0d5929df898f6d81a3d0f59272389d1b03367 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 18 Oct 2023 17:54:00 +0200 Subject: [PATCH] Add update logic --- client/ui/client_ui.go | 41 ++++++++++- version/update.go | 163 +++++++++++++++++++++++++++++++++++++++++ version/url.go | 46 ++++++++++++ 3 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 version/update.go create mode 100644 version/url.go diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 07d5b6a907d..14af2b9c8b8 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -101,6 +101,8 @@ type serviceClient struct { mDown *systray.MenuItem mAdminPanel *systray.MenuItem mSettings *systray.MenuItem + mVersion *systray.MenuItem + mUpdate *systray.MenuItem mQuit *systray.MenuItem // application with main windows. @@ -119,6 +121,8 @@ type serviceClient struct { managementURL string preSharedKey string adminURL string + + update *version.Update } // newServiceClient instance constructor @@ -131,6 +135,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient app: a, showSettings: showSettings, + update: version.NewUpdate(), } if runtime.GOOS == "windows" { @@ -342,6 +347,8 @@ func (s *serviceClient) updateStatus() error { s.mDown.Disable() s.mUp.Enable() } + + s.update.SetDaemonVersion(status.DaemonVersion) return nil }, &backoff.ExponentialBackOff{ InitialInterval: time.Second, @@ -377,11 +384,16 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() versionString := normalizedVersion() - v := systray.AddMenuItem(versionString, "Client Version: "+versionString) - v.Disable() + v := systray.AddMenuItem("About", "About") + s.mVersion = v.AddSubMenuItem(versionString, "Client Version: "+versionString) + s.mVersion.Disable() + s.mUpdate = v.AddSubMenuItem("Download latest version", "Download latest version") + s.mUpdate.Hide() + systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") + s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() for { @@ -439,6 +451,11 @@ func (s *serviceClient) onTrayReady() { case <-s.mQuit.ClickedCh: systray.Quit() return + case <-s.mUpdate.ClickedCh: + err := openURL(version.DownloadUrl()) + if err != nil { + log.Errorf("%s", err) + } } if err != nil { log.Errorf("process connection: %v", err) @@ -515,6 +532,26 @@ func (s *serviceClient) getSrvConfig() { } } +// todo hide after success daemon update +func (s *serviceClient) onUpdateAvailable(version string) { + s.mUpdate.Show() +} + +func openURL(url string) error { + var err error + switch runtime.GOOS { + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + case "linux": + err = exec.Command("xdg-open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + return err +} + // checkPIDFile exists and return error, or write new. func checkPIDFile() error { pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid") diff --git a/version/update.go b/version/update.go new file mode 100644 index 00000000000..b6bb80e1180 --- /dev/null +++ b/version/update.go @@ -0,0 +1,163 @@ +package version + +import ( + "io" + "net/http" + "sync" + "time" + + goversion "github.com/hashicorp/go-version" + log "github.com/sirupsen/logrus" +) + +const ( + versionURL = "https://pkgs.netbird.io/releases/latest/version" +) + +type Update struct { + uiVersion *goversion.Version + daemonVersion *goversion.Version + lastAvailable *goversion.Version + versionsLock sync.Mutex + + onUpdateListener func(version string) + listenerLock sync.Mutex +} + +func NewUpdate() *Update { + currentVersion, err := goversion.NewVersion(version) + if err != nil { + currentVersion, _ = goversion.NewVersion("0.0.0") + } + + lastAvailable, _ := goversion.NewVersion("0.0.0") + + u := &Update{ + lastAvailable: lastAvailable, + uiVersion: currentVersion, + } + + go u.startFetcher() + return u +} + +func (u *Update) SetDaemonVersion(newVersion string) { + 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 + } + + u.daemonVersion = daemonVersion + u.versionsLock.Unlock() + u.checkUpdate() + return +} + +func (u *Update) SetOnUpdateListener(updateFn func(version string)) { + u.listenerLock.Lock() + defer u.listenerLock.Unlock() + + u.onUpdateListener = updateFn + if u.isUpdateAvailable() { + u.onUpdateListener(version) + } +} + +func (u *Update) startFetcher() { + changed := u.fetchVersion() + if changed { + u.checkUpdate() + } + + uptimeTicker := time.NewTicker(30 * time.Minute) + for { + select { + case <-uptimeTicker.C: + changed := u.fetchVersion() + if changed { + u.checkUpdate() + } + + } + } + +} + +func (u *Update) fetchVersion() bool { + resp, err := http.Get(versionURL) + if err != nil { + log.Error("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 + } + + lastAvailable, err := goversion.NewVersion(string(content)) + if err != nil { + log.Errorf("faield to parse the version string: %s", err) + return false + } + + u.versionsLock.Lock() + defer u.versionsLock.Unlock() + + if u.lastAvailable.Equal(lastAvailable) { + return false + } + u.lastAvailable = lastAvailable + + return true +} + +func (u *Update) checkUpdate() { + if !u.isUpdateAvailable() { + return + } + + u.listenerLock.Lock() + defer u.listenerLock.Unlock() + if u.onUpdateListener == nil { + return + } + + u.onUpdateListener(u.lastAvailable.String()) +} + +func (u *Update) isUpdateAvailable() bool { + u.versionsLock.Lock() + defer u.versionsLock.Unlock() + + if u.lastAvailable.GreaterThan(u.uiVersion) { + return true + } + + if u.daemonVersion == nil { + return false + } + + if u.lastAvailable.GreaterThan(u.daemonVersion) { + return true + } + return false +} diff --git a/version/url.go b/version/url.go new file mode 100644 index 00000000000..ca3f85e05e7 --- /dev/null +++ b/version/url.go @@ -0,0 +1,46 @@ +package version + +import ( + "os/exec" + "runtime" +) + +const ( + downloadURL = "https://app.netbird.io/install" + macIntelURL = "https://pkgs.netbird.io/macos/amd64" + macM1M2URL = "https://pkgs.netbird.io/macos/arm64" +) + +func DownloadUrl() string { + switch runtime.GOOS { + case "windows": + return downloadURL + case "darwin": + return darwinDownloadUrl() + case "linux": + return downloadURL + default: + return downloadURL + } +} + +func darwinDownloadUrl() 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 macIntelURL + case "arm64": + return macM1M2URL + default: + return downloadURL + } +}