Split auto update settings and add support for registry state

This commit is contained in:
Daniel
2023-03-09 12:08:14 +01:00
parent d75d5092a3
commit 4802de61fa
9 changed files with 401 additions and 108 deletions

View File

@@ -94,7 +94,7 @@ func downloadUpdates() error {
}
// Download all required updates.
err = registry.DownloadUpdates(context.TODO())
err = registry.DownloadUpdates(context.TODO(), false)
if err != nil {
return err
}

View File

@@ -154,7 +154,7 @@ func verifyUpdates(ctx context.Context) error {
// Re-download broken files.
registry.MandatoryUpdates = helper.MandatoryUpdates()
registry.AutoUnpack = helper.AutoUnpackUpdates()
err = registry.DownloadUpdates(ctx)
err = registry.DownloadUpdates(ctx, false)
if err != nil {
return fmt.Errorf("failed to re-download files: %w", err)
}

View File

@@ -7,25 +7,24 @@ import (
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/updates/helper"
)
const (
cfgDevModeKey = "core/devMode"
updatesDisabledNotificationID = "updates:disabled"
)
const cfgDevModeKey = "core/devMode"
var (
releaseChannel config.StringOption
devMode config.BoolOption
enableUpdates config.BoolOption
releaseChannel config.StringOption
devMode config.BoolOption
enableSoftwareUpdates config.BoolOption
enableIntelUpdates config.BoolOption
initialReleaseChannel string
previousReleaseChannel string
updatesCurrentlyEnabled bool
previousDevMode bool
forceUpdate = abool.New()
initialReleaseChannel string
previousReleaseChannel string
softwareUpdatesCurrentlyEnabled bool
intelUpdatesCurrentlyEnabled bool
previousDevMode bool
forceUpdate = abool.New()
)
func registerConfig() error {
@@ -71,9 +70,9 @@ func registerConfig() error {
}
err = config.Register(&config.Option{
Name: "Automatic Updates",
Key: enableUpdatesKey,
Description: "Enable automatic checking, downloading and applying of updates. This affects all kinds of updates, including intelligence feeds and broadcast notifications.",
Name: "Automatic Software Updates",
Key: enableSoftwareUpdatesKey,
Description: "Automatically check for and download software updates. This does not include intelligence data updates.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
@@ -88,6 +87,24 @@ func registerConfig() error {
return err
}
err = config.Register(&config.Option{
Name: "Automatic Intelligence Data Updates",
Key: enableIntelUpdatesKey,
Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
RequiresRestart: false,
DefaultValue: true,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: -11,
config.CategoryAnnotation: "Updates",
},
})
if err != nil {
return err
}
return nil
}
@@ -96,37 +113,25 @@ func initConfig() {
initialReleaseChannel = releaseChannel()
previousReleaseChannel = releaseChannel()
enableUpdates = config.Concurrent.GetAsBool(enableUpdatesKey, true)
updatesCurrentlyEnabled = enableUpdates()
enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true)
enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true)
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
intelUpdatesCurrentlyEnabled = enableIntelUpdates()
devMode = config.Concurrent.GetAsBool(cfgDevModeKey, false)
previousDevMode = devMode()
}
func createWarningNotification() {
notifications.NotifyWarn(
updatesDisabledNotificationID,
"Automatic Updates Disabled",
"Automatic updates are disabled through configuration. Please note that this is potentially dangerous, as this also affects security updates as well as the filter lists and threat intelligence feeds.",
notifications.Action{
Text: "Change",
Type: notifications.ActionTypeOpenSetting,
Payload: &notifications.ActionTypeOpenSettingPayload{
Key: enableUpdatesKey,
},
},
).AttachToModule(module)
}
func updateRegistryConfig(_ context.Context, _ interface{}) error {
changed := false
if releaseChannel() != previousReleaseChannel {
previousReleaseChannel = releaseChannel()
warning := helper.SetIndexes(registry, releaseChannel(), true)
if warning != nil {
log.Warningf("updates: %s", warning)
}
if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled {
softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates()
changed = true
}
if enableIntelUpdates() != intelUpdatesCurrentlyEnabled {
intelUpdatesCurrentlyEnabled = enableIntelUpdates()
changed = true
}
@@ -136,24 +141,36 @@ func updateRegistryConfig(_ context.Context, _ interface{}) error {
changed = true
}
if enableUpdates() != updatesCurrentlyEnabled {
updatesCurrentlyEnabled = enableUpdates()
if releaseChannel() != previousReleaseChannel {
previousReleaseChannel = releaseChannel()
changed = true
}
if changed {
// Update indexes based on new settings.
warning := helper.SetIndexes(
registry,
releaseChannel(),
true,
softwareUpdatesCurrentlyEnabled,
intelUpdatesCurrentlyEnabled,
)
if warning != nil {
log.Warningf("updates: %s", warning)
}
// Select versions depending on new indexes and modes.
registry.SelectVersions()
module.TriggerEvent(VersionUpdateEvent, nil)
if updatesCurrentlyEnabled {
if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled {
module.Resolve("")
if err := TriggerUpdate(false); err != nil {
log.Warningf("updates: failed to trigger update: %s", err)
}
log.Infof("updates: automatic updates are now enabled")
} else {
createWarningNotification()
log.Warningf("updates: automatic updates are now disabled")
log.Warningf("updates: automatic updates are now completely disabled")
}
}

View File

@@ -21,6 +21,9 @@ const (
// versionsDBKey is the database key for simple update version information.
simpleVersionsDBKey = "core:status/simple-versions"
// updateStatusDBKey is the database key for update status information.
updateStatusDBKey = "core:status/updates"
)
// Versions holds update versions and status information.
@@ -50,6 +53,14 @@ type SimplifiedResourceVersion struct {
Version string
}
// UpdateStateExport is a wrapper to export the updates state.
type UpdateStateExport struct {
record.Base
sync.Mutex
*updater.UpdateState
}
// GetVersions returns the update versions and status information.
// Resources must be locked when accessed.
func GetVersions() *Versions {
@@ -98,6 +109,41 @@ func GetSimpleVersions() *SimpleVersions {
return v
}
// GetStateExport gets the update state from the registry and returns it in an
// exportable struct.
func GetStateExport() *UpdateStateExport {
export := registry.GetState()
return &UpdateStateExport{
UpdateState: &export.Updates,
}
}
// LoadStateExport loads the exported update state from the database.
func LoadStateExport() (*UpdateStateExport, error) {
r, err := db.Get(updateStatusDBKey)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
newRecord := &UpdateStateExport{}
err = record.Unwrap(r, newRecord)
if err != nil {
return nil, err
}
return newRecord, nil
}
// or adjust type
newRecord, ok := r.(*UpdateStateExport)
if !ok {
return nil, fmt.Errorf("record not of type *UpdateStateExport, but %T", r)
}
return newRecord, nil
}
func initVersionExport() (err error) {
if err := GetVersions().save(); err != nil {
log.Warningf("updates: failed to export version information: %s", err)
@@ -128,12 +174,28 @@ func (v *SimpleVersions) save() error {
return db.Put(v)
}
func (s *UpdateStateExport) save() error {
if !s.KeyIsSet() {
s.SetKey(updateStatusDBKey)
}
return db.Put(s)
}
// export is an event hook.
func export(_ context.Context, _ interface{}) error {
// Export versions.
if err := GetVersions().save(); err != nil {
return err
}
return GetSimpleVersions().save()
if err := GetSimpleVersions().save(); err != nil {
return err
}
// Export udpate state.
if err := GetStateExport().save(); err != nil {
return err
}
return nil
}
// AddToDebugInfo adds the update system status to the given debug.Info.

View File

@@ -32,3 +32,30 @@ func GetFile(identifier string) (*updater.File, error) {
module.TriggerEvent(VersionUpdateEvent, nil)
return file, nil
}
// GetPlatformVersion returns the selected platform specific version of the
// given identifier.
// The returned resource version may not be modified.
func GetPlatformVersion(identifier string) (*updater.ResourceVersion, error) {
identifier = helper.PlatformIdentifier(identifier)
rv, err := registry.GetVersion(identifier)
if err != nil {
return nil, err
}
return rv, nil
}
// GetVersion returns the selected generic version of the given identifier.
// The returned resource version may not be modified.
func GetVersion(identifier string) (*updater.ResourceVersion, error) {
identifier = path.Join("all", identifier)
rv, err := registry.GetVersion(identifier)
if err != nil {
return nil, err
}
return rv, nil
}

View File

@@ -27,7 +27,13 @@ const (
// SetIndexes sets the update registry indexes and also configures the registry
// to use pre-releases based on the channel.
func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string, deleteUnusedIndexes bool) (warning error) {
func SetIndexes(
registry *updater.ResourceRegistry,
releaseChannel string,
deleteUnusedIndexes bool,
autoDownload bool,
autoDownloadIntel bool,
) (warning error) {
usePreReleases := false
// Be reminded that the order is important, as indexes added later will
@@ -39,12 +45,14 @@ func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string, delet
// Add the intel index first, in order to be able to override it with the
// other indexes when needed.
registry.AddIndex(updater.Index{
Path: "all/intel/intel.json",
Path: "all/intel/intel.json",
AutoDownload: autoDownloadIntel,
})
// Always add the stable index as a base.
registry.AddIndex(updater.Index{
Path: ReleaseChannelStable + ".json",
Path: ReleaseChannelStable + ".json",
AutoDownload: autoDownload,
})
// Add beta index if in beta or staging channel.
@@ -53,8 +61,9 @@ func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string, delet
releaseChannel == ReleaseChannelStaging ||
(releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{
Path: indexPath,
PreRelease: true,
Path: indexPath,
PreRelease: true,
AutoDownload: autoDownload,
})
usePreReleases = true
} else if deleteUnusedIndexes {
@@ -69,8 +78,9 @@ func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string, delet
if releaseChannel == ReleaseChannelStaging ||
(releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{
Path: indexPath,
PreRelease: true,
Path: indexPath,
PreRelease: true,
AutoDownload: autoDownload,
})
usePreReleases = true
} else if deleteUnusedIndexes {
@@ -85,7 +95,8 @@ func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string, delet
if releaseChannel == ReleaseChannelSupport ||
(releaseChannel == "" && indexExists(registry, indexPath)) {
registry.AddIndex(updater.Index{
Path: indexPath,
Path: indexPath,
AutoDownload: autoDownload,
})
} else if deleteUnusedIndexes {
err := deleteIndex(registry, indexPath)

View File

@@ -5,14 +5,12 @@ import (
"flag"
"fmt"
"runtime"
"sync/atomic"
"time"
"github.com/safing/portbase/database"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/notifications"
"github.com/safing/portbase/updater"
"github.com/safing/portmaster/updates/helper"
)
@@ -20,7 +18,8 @@ import (
const (
onWindows = runtime.GOOS == "windows"
enableUpdatesKey = "core/automaticUpdates"
enableSoftwareUpdatesKey = "core/automaticUpdates"
enableIntelUpdatesKey = "core/automaticIntelUpdates"
// ModuleName is the name of the update module
// and can be used when declaring module dependencies.
@@ -64,9 +63,6 @@ var (
const (
updatesDirName = "updates"
updateFailed = "updates:failed"
updateSuccess = "updates:success"
updateTaskRepeatDuration = 1 * time.Hour
)
@@ -119,14 +115,39 @@ func start() error {
// override with flag value
registry.UserAgent = userAgentFromFlag
}
// pre-init state
updateStateExport, err := LoadStateExport()
if err != nil {
log.Debugf("updates: failed to load exported update state: %s", err)
} else if updateStateExport.UpdateState != nil {
err := registry.PreInitUpdateState(*updateStateExport.UpdateState)
if err != nil {
return err
}
}
// initialize
err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755))
err = registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755))
if err != nil {
return err
}
// register state provider
err = registerRegistryStateProvider()
if err != nil {
return err
}
registry.StateNotifyFunc = pushRegistryState
// Set indexes based on the release channel.
warning := helper.SetIndexes(registry, initialReleaseChannel, true)
warning := helper.SetIndexes(
registry,
initialReleaseChannel,
true,
enableSoftwareUpdates(),
enableIntelUpdates(),
)
if warning != nil {
log.Warningf("updates: %s", warning)
}
@@ -144,10 +165,6 @@ func start() error {
registry.SelectVersions()
module.TriggerEvent(VersionUpdateEvent, nil)
if !updatesCurrentlyEnabled {
createWarningNotification()
}
// Initialize the version export - this requires the registry to be set up.
err = initVersionExport()
if err != nil {
@@ -185,11 +202,13 @@ func TriggerUpdate(force bool) error {
case !module.Online():
updateASAP = true
case !force && !enableUpdates():
case !force && !enableSoftwareUpdates() && !enableIntelUpdates():
return fmt.Errorf("automatic updating is disabled")
default:
forceUpdate.Set()
if force {
forceUpdate.Set()
}
updateTask.StartASAP()
}
@@ -211,8 +230,6 @@ func DisableUpdateSchedule() error {
return nil
}
var updateFailedCnt = new(atomic.Int32)
func checkForUpdates(ctx context.Context) (err error) {
// Set correct error if context was canceled.
defer func() {
@@ -223,7 +240,8 @@ func checkForUpdates(ctx context.Context) (err error) {
}
}()
if !forceUpdate.SetToIf(true, false) && !enableUpdates() {
forcedUpdate := forceUpdate.SetToIf(true, false)
if !forcedUpdate && !enableSoftwareUpdates() && !enableIntelUpdates() {
log.Warningf("updates: automatic updates are disabled")
return nil
}
@@ -231,45 +249,14 @@ func checkForUpdates(ctx context.Context) (err error) {
defer func() {
// Resolve any error and and send succes notification.
if err == nil {
updateFailedCnt.Store(0)
log.Infof("updates: successfully checked for updates")
module.Resolve(updateFailed)
notifications.Notify(&notifications.Notification{
EventID: updateSuccess,
Type: notifications.Info,
Title: "Update Check Successful",
Message: "The Portmaster successfully checked for updates and downloaded any available updates. Most updates are applied automatically. You will be notified of important updates that need restarting.",
Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
},
})
notifyUpdateSuccess(forcedUpdate)
return
}
// Log error in any case.
// Log and notify error.
log.Errorf("updates: check failed: %s", err)
// Do not alert user if update failed for only a few times.
if updateFailedCnt.Add(1) > 3 {
notifications.NotifyWarn(
updateFailed,
"Update Check Failed",
"The Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. If you just installed the Portmaster, please try disabling potentially conflicting software, such as other firewalls or VPNs.",
notifications.Action{
ID: "retry",
Text: "Try Again Now",
Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates,
ResultAction: "display",
},
},
).AttachToModule(module)
}
notifyUpdateCheckFailed(forcedUpdate, err)
}()
if err = registry.UpdateIndexes(ctx); err != nil {
@@ -277,7 +264,7 @@ func checkForUpdates(ctx context.Context) (err error) {
return
}
err = registry.DownloadUpdates(ctx)
err = registry.DownloadUpdates(ctx, !forcedUpdate)
if err != nil {
err = fmt.Errorf("failed to download updates: %w", err)
return
@@ -293,7 +280,7 @@ func checkForUpdates(ctx context.Context) (err error) {
}
// Purge old resources
registry.Purge(3)
registry.Purge(2)
module.TriggerEvent(ResourceUpdateEvent, nil)
return nil

140
updates/notify.go Normal file
View File

@@ -0,0 +1,140 @@
package updates
import (
"fmt"
"sync/atomic"
"time"
"github.com/safing/portbase/notifications"
)
const (
updateFailed = "updates:failed"
updateSuccess = "updates:success"
updateSuccessPending = "updates:success-pending"
updateSuccessDownloaded = "updates:success-downloaded"
failedUpdateNotifyDurationThreshold = 24 * time.Hour
failedUpdateNotifyCountThreshold = 3
)
var updateFailedCnt = new(atomic.Int32)
func notifyUpdateSuccess(forced bool) {
updateFailedCnt.Store(0)
module.Resolve(updateFailed)
updateState := registry.GetState().Updates
flavor := updateSuccess
switch {
case len(updateState.PendingDownload) > 0:
// Show notification if there are pending downloads.
flavor = updateSuccessPending
case updateState.LastDownloadAt != nil &&
time.Since(*updateState.LastDownloadAt) < time.Minute:
// Show notification if we downloaded something within the last minute.
flavor = updateSuccessDownloaded
case forced:
// Always show notification if update was manually triggered.
default:
// Otherwise, the update was uneventful. Do not show notification.
return
}
switch flavor {
case updateSuccess:
notifications.Notify(&notifications.Notification{
EventID: updateSuccess,
Type: notifications.Info,
Title: "Portmaster Is Up-To-Date",
Message: "Portmaster successfully checked for updates. Everything is up to date. Most updates are applied automatically. You will be notified of important updates that need restarting.",
Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
},
})
case updateSuccessPending:
notifications.Notify(&notifications.Notification{
EventID: updateSuccess,
Type: notifications.Info,
Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)),
Message: fmt.Sprintf(
`%d updates are available for download. Press "Download Now" or check for updates later to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`,
len(updateState.PendingDownload),
),
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
{
ID: "download",
Text: "Download Now",
Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates,
ResultAction: "display",
},
},
},
})
case updateSuccessDownloaded:
notifications.Notify(&notifications.Notification{
EventID: updateSuccess,
Type: notifications.Info,
Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)),
Message: fmt.Sprintf(
`%d updates were downloaded and applied. You will be notified of important updates that need restarting.`,
len(updateState.LastDownload),
),
Expires: time.Now().Add(1 * time.Minute).Unix(),
AvailableActions: []*notifications.Action{
{
ID: "ack",
Text: "OK",
},
},
})
}
}
func notifyUpdateCheckFailed(forced bool, err error) {
failedCnt := updateFailedCnt.Add(1)
lastSuccess := registry.GetState().Updates.LastSuccessAt
switch {
case forced:
// Always show notification if update was manually triggered.
case failedCnt < failedUpdateNotifyCountThreshold:
// Not failed often enough for notification.
return
case lastSuccess == nil:
// No recorded successful udpate.
case time.Now().Add(-failedUpdateNotifyDurationThreshold).Before(*lastSuccess):
// Failed too recently for notification.
return
}
notifications.NotifyWarn(
updateFailed,
"Update Check Failed",
fmt.Sprintf(
"Portmaster failed to check for updates. This might be a temporary issue of your device, your network or the update servers. The Portmaster will automatically try again later. The error was: %s",
err,
),
notifications.Action{
Text: "Try Again Now",
Type: notifications.ActionTypeWebhook,
Payload: &notifications.ActionTypeWebhookPayload{
URL: apiPathCheckForUpdates,
ResultAction: "display",
},
},
).AttachToModule(module)
}

49
updates/state.go Normal file
View File

@@ -0,0 +1,49 @@
package updates
import (
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/runtime"
"github.com/safing/portbase/updater"
)
var pushRegistryStatusUpdate runtime.PushFunc
// RegistryStateExport is a wrapper to export the registry state.
type RegistryStateExport struct {
record.Base
*updater.RegistryState
}
func exportRegistryState(s *updater.RegistryState) *RegistryStateExport {
if s == nil {
state := registry.GetState()
s = &state
}
export := &RegistryStateExport{
RegistryState: s,
}
export.CreateMeta()
export.SetKey("runtime:core/updates/state")
return export
}
func pushRegistryState(s *updater.RegistryState) {
export := exportRegistryState(s)
pushRegistryStatusUpdate(export)
}
func registerRegistryStateProvider() (err error) {
registryStateProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) {
return []record.Record{exportRegistryState(nil)}, nil
})
pushRegistryStatusUpdate, err = runtime.Register("core/updates/state", registryStateProvider)
if err != nil {
return err
}
return nil
}