[WIP] Error and state handleing improvments, better logs

This commit is contained in:
Vladimir Stoilov
2024-09-19 12:38:23 +03:00
parent 072d7e6971
commit b3ff6f14f1
24 changed files with 146 additions and 1981 deletions

View File

@@ -1,10 +1,12 @@
package updates
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"runtime"
"time"
semver "github.com/hashicorp/go-version"
@@ -15,7 +17,29 @@ import (
"github.com/safing/portmaster/service/mgr"
)
const updateAvailableNotificationID = "updates:update-available"
const (
updateTaskRepeatDuration = 1 * time.Hour
updateAvailableNotificationID = "updates:update-available"
// VersionUpdateEvent is emitted every time a new
// version of a monitored resource is selected.
// During module initialization VersionUpdateEvent
// is also emitted.
VersionUpdateEvent = "active version update"
// ResourceUpdateEvent is emitted every time the
// updater successfully performed a resource update.
// ResourceUpdateEvent is emitted even if no new
// versions are available. Subscribers are expected
// to check if new versions of their resources are
// available by checking File.UpgradeAvailable().
ResourceUpdateEvent = "resource update"
)
// UserAgent is an HTTP User-Agent that is used to add
// more context to requests made by the registry when
// fetching resources from the update server.
var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH)
type File struct {
id string
@@ -75,7 +99,7 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) {
// Events
module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil)
module.updateCheckWorkerMgr.Repeat(1 * time.Hour)
module.updateCheckWorkerMgr.Repeat(updateTaskRepeatDuration)
module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", module.applyUpdates, nil)
var err error
@@ -86,44 +110,10 @@ func New(instance instance, name string, index UpdateIndex) (*Updates, error) {
// Add bundle artifacts to registry.
module.processBundle(module.bundle)
err = module.registerEndpoints()
if err != nil {
log.Errorf("failed to register endpoints: %s", err)
}
return module, nil
}
func (u *Updates) registerEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Check for update",
Description: "Trigger update check",
Path: "updates/check",
Read: api.PermitAnyone,
ActionFunc: func(ar *api.Request) (msg string, err error) {
u.updateCheckWorkerMgr.Go()
return "Check for updates triggered", nil
},
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Apply update",
Description: "Triggers update",
Path: "updates/apply",
Read: api.PermitAnyone,
ActionFunc: func(ar *api.Request) (msg string, err error) {
u.upgraderWorkerMgr.Go()
return "Apply updates triggered", nil
},
}); err != nil {
return err
}
return nil
}
func (reg *Updates) processBundle(bundle *Bundle) {
for _, artifact := range bundle.Artifacts {
artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename)
@@ -131,9 +121,9 @@ func (reg *Updates) processBundle(bundle *Bundle) {
}
}
func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error {
func (u *Updates) checkForUpdates(wc *mgr.WorkerCtx) error {
httpClient := http.Client{}
err := u.updateIndex.DownloadIndexFile(&httpClient)
err := u.updateIndex.DownloadIndexFile(wc.Ctx(), &httpClient)
if err != nil {
return fmt.Errorf("failed to download index file: %s", err)
}
@@ -155,16 +145,23 @@ func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error {
}
log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version)
err = u.downloadUpdates(&httpClient)
err = u.downloadUpdates(wc.Ctx(), &httpClient)
if err != nil {
log.Errorf("updates: failed to download bundle: %s", err)
} else {
notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{
ID: "apply",
Text: "Apply",
Type: notifications.ActionTypeInjectEvent,
Payload: "apply-updates",
})
if u.updateIndex.AutoApply {
u.upgraderWorkerMgr.Go()
} else {
notifications.NotifyPrompt(updateAvailableNotificationID, "Update available", "Apply update and restart?", notifications.Action{
ID: "apply",
Text: "Apply",
Type: notifications.ActionTypeWebhook,
Payload: notifications.ActionTypeWebhookPayload{
Method: "POST",
URL: "updates/apply",
},
})
}
}
return nil
}
@@ -183,7 +180,7 @@ func (u *Updates) checkVersionIncrement() (bool, error) {
return downloadVersion.GreaterThan(currentVersion), nil
}
func (u *Updates) downloadUpdates(client *http.Client) error {
func (u *Updates) downloadUpdates(ctx context.Context, client *http.Client) error {
if u.updateBundle == nil {
// checkForUpdates needs to be called before this.
return fmt.Errorf("no valid update bundle found")
@@ -193,7 +190,7 @@ func (u *Updates) downloadUpdates(client *http.Client) error {
if err != nil {
log.Warningf("updates: error while coping file from current to update: %s", err)
}
u.updateBundle.DownloadAndVerify(client, u.updateIndex.DownloadDirectory)
u.updateBundle.DownloadAndVerify(ctx, client, u.updateIndex.DownloadDirectory)
return nil
}
@@ -208,18 +205,23 @@ func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error {
return fmt.Errorf("there is no new version to apply")
}
// Verify files of the downloaded files.
err = u.updateBundle.Verify(u.updateIndex.DownloadDirectory)
if err != nil {
return fmt.Errorf("failed to apply update: %s", err)
return fmt.Errorf("failed to verify downloaded files: %s", err)
}
// New version is downloaded and verified. Start the update process
log.Infof("update: starting update: %s %s -> %s", u.bundle.Name, u.bundle.Version, u.updateBundle.Version)
err = switchFolders(u.updateIndex, *u.updateBundle)
if err != nil {
// TODO(vladimir): Send notification to UI
log.Errorf("updates: failed to apply updates: %s", err)
} else {
// TODO(vladimir): Prompt user to restart?
u.instance.Restart()
if u.updateIndex.NeedsRestart {
u.instance.Restart()
}
}
return nil
}
@@ -274,7 +276,7 @@ func (u *Updates) GetFile(id string) (*File, error) {
// Stop stops the module.
func (u *Updates) Stop() error {
return stop()
return nil
}
type instance interface {