From 5b20b6a3d0e0488fe37a87a733b82c3de6b5c0b0 Mon Sep 17 00:00:00 2001 From: Jakub Date: Wed, 30 Mar 2022 14:34:58 +0200 Subject: [PATCH] GODT-1537: Manual in-app update mechanism. --- doc/updates.md | 103 ++++++++++++++++++ internal/frontend/qml/Bridge.qml | 2 +- internal/frontend/qml/Bridge_test.qml | 17 ++- internal/frontend/qml/MainWindow.qml | 1 + internal/frontend/qml/NotificationPopups.qml | 4 + .../qml/Notifications/Notifications.qml | 9 +- internal/frontend/qt/frontend.go | 2 +- internal/frontend/qt/frontend_updates.go | 35 +++++- internal/frontend/qt/qml_backend.go | 8 ++ 9 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 doc/updates.md diff --git a/doc/updates.md b/doc/updates.md new file mode 100644 index 00000000..e63116f3 --- /dev/null +++ b/doc/updates.md @@ -0,0 +1,103 @@ +# Update mechanism of Bridge + +There are mulitple options how to change version of application: +* Automatic in-app update +* Manual in-app update +* Manual install + +In-app update ends with restarting bridge into new version. Automatic in-app +update is downloading, verifying and installing the new version immediatelly +without user confirmation. For manual in-app update user needs to confirm first. +Update is done from special update file published on website. + +The manual installation requires user to download, verify and install manually +using installer for given OS. + +The bridge is installed and executed differently for given OS: + +* Windows and Linux apps are using launcher mechanism: + * There is system protected installation path which is created on first + install. It contains bridge exe and launcher exe. When users starts + bridge the launcher is executed first. It will check update path compare + version with installed one. The newer version then is then executed. + * Update mechanism means to replace files in update folder which is located + in user space. + +* macOS app does not use launcher + * No launcher, only one executable + * In-App udpate replaces the bridge files in installation path directly + + +```mermaid +flowchart LR + subgraph Frontend + U[User requests
version check] + ManIns((Notify user about
manual install
is needed)) + R((Notify user
about restart)) + ManUp((Notify user about
manual update)) + NF((Notify user about
force update)) + + ManUp -->|Install| InstFront[Install] + InstFront -->|Ok| R + InstFront -->|Error| ManIns + + U --> CheckFront[Check online] + CheckFront -->|Ok| IAFront{Is new version
and applicable?} + CheckFront -->|Error| ManIns + + IAFront -->|No| Latest((Notify user
has latest version)) + IAFront -->|Yes| CanInstall{Can update?} + CanInstall -->|No| ManIns + CanInstall -->|Yes| NotifOrInstall{Is automatic
update enabled?} + NotifOrInstall -->|Manual| ManUp + end + + + subgraph Backend + W[Wait for next check] + + W --> Check[Check online] + + Check --> NV{Has new
version?} + Check -->|Error| W + NV -->|No new version| W + IA{Is install
applicable?} + NV -->|New version
available| IA + IA -->|Local rollout
not enough| W + IA -->|Yes| AU{Is automatic\nupdate enabled?} + + AU -->|Yes| CanUp{Can update?} + CanUp -->|No| ManIns + + CanUp -->|Yes| Ins[Install] + Ins -->|Error| ManIns + Ins -->|Ok| R + + AU -->|No| ManUp + ManUp -->|Ignore| W + + + F[Force update] + F --> NF + end + + ManIns --> Web[Open web page] + NF --> Web + ManUp --> Web + R --> Re[Restart] + NF --> Q[Quit bridge] + NotifOrInstall -->|Automatic| W +``` + + +The non-trivial is to combine the update with setting change: +* turn off/on automatic in-app updates +* change from stable to beta or back + +_TODO fill flow chart details_ + + +We are not support downgrade functionality. Only some circumstances can lead to +downgrading the app version. + +_TODO fill flow chart details_ diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml index 110afb64..7deb44c2 100644 --- a/internal/frontend/qml/Bridge.qml +++ b/internal/frontend/qml/Bridge.qml @@ -133,7 +133,7 @@ QtObject { return Qt.point(_x, _y) } - // fir to the right + // fit to the right _x = iconRect.right if (isInInterval(_x, screenRect.left, screenRect.right - width)) { // position preferebly in the vertical center but bound to the screen rect diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml index b7040386..187b020d 100644 --- a/internal/frontend/qml/Bridge_test.qml +++ b/internal/frontend/qml/Bridge_test.qml @@ -729,6 +729,9 @@ Window { console.log("check updates") } signal checkUpdatesFinished() + function installUpdate() { + console.log("manuall install update triggered") + } property bool isDiskCacheEnabled: true @@ -748,7 +751,19 @@ Window { property bool isAutomaticUpdateOn : true function toggleAutomaticUpdate(makeItActive) { console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn) - root.isAutomaticUpdateOn = makeItActive + var callback = function () { + root.isAutomaticUpdateOn = makeItActive; + console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn) + } + atimer.onTriggered.connect(callback) + atimer.restart() + } + + Timer { + id: atimer + interval: 2000 + running: false + repeat: false } property bool isAutostartOn : true // Example of settings with loading state diff --git a/internal/frontend/qml/MainWindow.qml b/internal/frontend/qml/MainWindow.qml index 7ad1e426..02d1c52f 100644 --- a/internal/frontend/qml/MainWindow.qml +++ b/internal/frontend/qml/MainWindow.qml @@ -182,6 +182,7 @@ ApplicationWindow { colorScheme: root.colorScheme notifications: root.notifications mainWindow: root + backend: root.backend } function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() } diff --git a/internal/frontend/qml/NotificationPopups.qml b/internal/frontend/qml/NotificationPopups.qml index 0e7dab1d..35d7df07 100644 --- a/internal/frontend/qml/NotificationPopups.qml +++ b/internal/frontend/qml/NotificationPopups.qml @@ -25,6 +25,7 @@ import Notifications 1.0 Item { id: root + property var backend property ColorScheme colorScheme property var notifications @@ -51,8 +52,11 @@ Item { notification: root.notifications.updateManualReady Switch { + id:autoUpdate colorScheme: root.colorScheme text: qsTr("Update automatically in the future") + checked: root.backend.isAutomaticUpdateOn + onClicked: root.backend.toggleAutomaticUpdate(autoUpdate.checked) } } diff --git a/internal/frontend/qml/Notifications/Notifications.qml b/internal/frontend/qml/Notifications/Notifications.qml index 36ee23cd..2dc366db 100644 --- a/internal/frontend/qml/Notifications/Notifications.qml +++ b/internal/frontend/qml/Notifications/Notifications.qml @@ -166,8 +166,9 @@ QtObject { } property Notification updateManualError: Notification { - description: qsTr("Bridge couldn’t update") - brief: description + title: qsTr("Bridge couldn’t update") + brief: title + description: qsTr("Please follow manual installation in order to update Bridge.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Warning group: Notifications.Group.Update @@ -192,7 +193,7 @@ QtObject { text: qsTr("Remind me later") onTriggered: { - root.updateManualReady.active = false + root.updateManualError.active = false } } ] @@ -273,7 +274,7 @@ QtObject { onTriggered: { root.backend.quit() - root.updateForce.active = false + root.updateForceError.active = false } } ] diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go index 0383d1c0..bbfc966d 100644 --- a/internal/frontend/qt/frontend.go +++ b/internal/frontend/qt/frontend.go @@ -153,7 +153,7 @@ func (f *FrontendQt) NotifySilentUpdateInstalled() { } func (f *FrontendQt) NotifySilentUpdateError(err error) { - f.log.WithError(err).Warn("Update failed, asking for manual.") + f.log.WithError(err).Warn("In-app update failed, asking for manual.") f.qml.UpdateManualError() } diff --git a/internal/frontend/qt/frontend_updates.go b/internal/frontend/qt/frontend_updates.go index 90bb16a6..3c48f1ad 100644 --- a/internal/frontend/qt/frontend_updates.go +++ b/internal/frontend/qt/frontend_updates.go @@ -25,6 +25,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/updater" + "github.com/pkg/errors" ) var checkingUpdates = sync.Mutex{} @@ -62,9 +63,18 @@ func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) { if !f.updater.CanInstall(f.newVersionInfo) { f.log.Debug("A manual update is required") - f.qml.UpdateManualReady(f.newVersionInfo.Version.String()) + f.qml.UpdateManualError() return } + + if f.settings.GetBool(settings.AutoUpdateKey) { + // NOOP will update eventually + return + } + + if isRequestFromUser { + f.qml.UpdateManualReady(f.newVersionInfo.Version.String()) + } } func (f *FrontendQt) updateForce() { @@ -113,3 +123,26 @@ func (f *FrontendQt) toggleBeta(makeItEnabled bool) { // Immediately check the updates to set the correct landing page link. f.checkUpdates() } + +func (f *FrontendQt) installUpdate() { + checkingUpdates.Lock() + defer checkingUpdates.Unlock() + + if !f.updater.CanInstall(f.newVersionInfo) { + f.log.Warning("Skipping update installation, current version too old") + f.qml.UpdateManualError() + return + } + + if err := f.updater.InstallUpdate(f.newVersionInfo); err != nil { + if errors.Cause(err) == updater.ErrDownloadVerify { + f.log.WithError(err).Warning("Skipping update installation due to temporary error") + } else { + f.log.WithError(err).Error("The update couldn't be installed") + f.qml.UpdateManualError() + } + return + } + + f.qml.UpdateSilentRestartNeeded() +} diff --git a/internal/frontend/qt/qml_backend.go b/internal/frontend/qt/qml_backend.go index a9c7ac2e..23292b71 100644 --- a/internal/frontend/qt/qml_backend.go +++ b/internal/frontend/qt/qml_backend.go @@ -81,6 +81,7 @@ type QMLBackend struct { _ func() `signal:"updateIsLatestVersion"` _ func() `slot:"checkUpdates"` _ func() `signal:"checkUpdatesFinished"` + _ func() `slot:"installUpdate"` _ bool `property:"isDiskCacheEnabled"` _ core.QUrl `property:"diskCachePath"` @@ -213,6 +214,13 @@ func (q *QMLBackend) setup(f *FrontendQt) { }() }) + q.ConnectInstallUpdate(func() { + go func() { + defer f.panicHandler.HandlePanic() + f.installUpdate() + }() + }) + f.setIsDiskCacheEnabled() f.setDiskCachePath() q.ConnectChangeLocalCache(func(e bool, d *core.QUrl) {