GODT-1537: Manual in-app update mechanism.

This commit is contained in:
Jakub 2022-03-30 14:34:58 +02:00
parent 3b07121f08
commit 5b20b6a3d0
9 changed files with 173 additions and 8 deletions

103
doc/updates.md Normal file
View File

@ -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<br>version check]
ManIns((Notify user about<br>manual install<br>is needed))
R((Notify user<br>about restart))
ManUp((Notify user about<br>manual update))
NF((Notify user about<br>force update))
ManUp -->|Install| InstFront[Install]
InstFront -->|Ok| R
InstFront -->|Error| ManIns
U --> CheckFront[Check online]
CheckFront -->|Ok| IAFront{Is new version<br>and applicable?}
CheckFront -->|Error| ManIns
IAFront -->|No| Latest((Notify user<br>has latest version))
IAFront -->|Yes| CanInstall{Can update?}
CanInstall -->|No| ManIns
CanInstall -->|Yes| NotifOrInstall{Is automatic<br>update enabled?}
NotifOrInstall -->|Manual| ManUp
end
subgraph Backend
W[Wait for next check]
W --> Check[Check online]
Check --> NV{Has new<br>version?}
Check -->|Error| W
NV -->|No new version| W
IA{Is install<br>applicable?}
NV -->|New version<br>available| IA
IA -->|Local rollout<br>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_

View File

@ -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

View File

@ -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

View File

@ -182,6 +182,7 @@ ApplicationWindow {
colorScheme: root.colorScheme
notifications: root.notifications
mainWindow: root
backend: root.backend
}
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }

View File

@ -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)
}
}

View File

@ -166,8 +166,9 @@ QtObject {
}
property Notification updateManualError: Notification {
description: qsTr("Bridge couldnt update")
brief: description
title: qsTr("Bridge couldnt 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
}
}
]

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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) {