From b3ff6f14f173b5ce412b12baf81facfb984a8958 Mon Sep 17 00:00:00 2001 From: Vladimir Stoilov Date: Thu, 19 Sep 2024 12:38:23 +0300 Subject: [PATCH] [WIP] Error and state handleing improvments, better logs --- service/core/api.go | 14 +- service/firewall/api.go | 4 +- service/instance.go | 6 +- service/process/module.go | 8 - service/process/profile.go | 27 +- service/profile/module.go | 12 +- service/updates/api.go | 161 --------- service/updates/bundle.go | 26 +- service/updates/config.go | 178 ---------- service/updates/export.go | 237 ------------- service/updates/get.go | 65 ---- service/updates/helper/electron.go | 58 ---- service/updates/helper/indexes.go | 136 -------- service/updates/helper/signing.go | 42 --- service/updates/helper/updates.go | 95 ----- service/updates/index.go | 19 +- service/updates/main.go | 69 ---- service/updates/module.go | 102 +++--- service/updates/notify.go | 176 ---------- service/updates/os_integration_default.go | 8 - service/updates/os_integration_linux.go | 201 ----------- service/updates/state.go | 49 --- service/updates/updater.go | 31 +- service/updates/upgrader.go | 403 ---------------------- 24 files changed, 146 insertions(+), 1981 deletions(-) delete mode 100644 service/updates/api.go delete mode 100644 service/updates/config.go delete mode 100644 service/updates/export.go delete mode 100644 service/updates/get.go delete mode 100644 service/updates/helper/electron.go delete mode 100644 service/updates/helper/indexes.go delete mode 100644 service/updates/helper/signing.go delete mode 100644 service/updates/helper/updates.go delete mode 100644 service/updates/notify.go delete mode 100644 service/updates/os_integration_default.go delete mode 100644 service/updates/os_integration_linux.go delete mode 100644 service/updates/state.go delete mode 100644 service/updates/upgrader.go diff --git a/service/core/api.go b/service/core/api.go index aa1305e0..2c465ccc 100644 --- a/service/core/api.go +++ b/service/core/api.go @@ -107,27 +107,29 @@ func registerAPIEndpoints() error { } if err := api.RegisterEndpoint(api.Endpoint{ - Path: "updates/check", - Read: api.PermitUser, + Path: "updates/check", + WriteMethod: "POST", + Write: api.PermitUser, ActionFunc: func(ar *api.Request) (string, error) { module.instance.BinaryUpdates().TriggerUpdateCheck() module.instance.IntelUpdates().TriggerUpdateCheck() return "update check triggered", nil }, - Name: "Get the ID of the calling profile", + Name: "Trigger updates check event", }); err != nil { return err } if err := api.RegisterEndpoint(api.Endpoint{ - Path: "updates/apply", - Read: api.PermitUser, + Path: "updates/apply", + WriteMethod: "POST", + Write: api.PermitUser, ActionFunc: func(ar *api.Request) (string, error) { module.instance.BinaryUpdates().TriggerApplyUpdates() module.instance.IntelUpdates().TriggerApplyUpdates() return "upgrade triggered", nil }, - Name: "Get the ID of the calling profile", + Name: "Trigger updates apply event", }); err != nil { return err } diff --git a/service/firewall/api.go b/service/firewall/api.go index 134f6f74..f16b7acd 100644 --- a/service/firewall/api.go +++ b/service/firewall/api.go @@ -18,7 +18,6 @@ import ( "github.com/safing/portmaster/service/network/netutils" "github.com/safing/portmaster/service/network/packet" "github.com/safing/portmaster/service/process" - "github.com/safing/portmaster/service/updates" ) const ( @@ -133,7 +132,8 @@ func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bo var originalPid int // Get authenticated path. - authenticatedPath := updates.RootPath() + // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. + authenticatedPath := "" // updates.RootPath() if authenticatedPath == "" { return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user } diff --git a/service/instance.go b/service/instance.go index c514fd80..f687062b 100644 --- a/service/instance.go +++ b/service/instance.go @@ -133,13 +133,14 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx return nil, err } binaryUpdateIndex = updates.UpdateIndex{ - Directory: binaryFolder, // Default: C:/Program Files/Portmaster/binary + Directory: binaryFolder, // Default: C:/Program Files/Portmaster DownloadDirectory: os.ExpandEnv("%ProgramData%/Portmaster/new_binary"), PurgeDirectory: os.ExpandEnv("%ProgramData%/Portmaster/old_binary"), Ignore: []string{"databases", "intel", "config.json"}, IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"}, IndexFile: "bin-index.json", AutoApply: false, + NeedsRestart: true, } intelUpdateIndex = updates.UpdateIndex{ @@ -149,6 +150,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, + NeedsRestart: false, } } else if go_runtime.GOOS == "linux" { binaryUpdateIndex = updates.UpdateIndex{ @@ -159,6 +161,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://localhost:8000/test-binary.json"}, IndexFile: "bin-index.json", AutoApply: false, + NeedsRestart: true, } intelUpdateIndex = updates.UpdateIndex{ @@ -168,6 +171,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx IndexURLs: []string{"http://localhost:8000/test-intel.json"}, IndexFile: "intel-index.json", AutoApply: true, + NeedsRestart: false, } } diff --git a/service/process/module.go b/service/process/module.go index 563368ab..b17a0c05 100644 --- a/service/process/module.go +++ b/service/process/module.go @@ -2,11 +2,9 @@ package process import ( "errors" - "os" "sync/atomic" "github.com/safing/portmaster/service/mgr" - "github.com/safing/portmaster/service/updates" ) type ProcessModule struct { @@ -19,10 +17,6 @@ func (pm *ProcessModule) Manager() *mgr.Manager { } func (pm *ProcessModule) Start() error { - updatesPath = updates.RootPath() - if updatesPath != "" { - updatesPath += string(os.PathSeparator) - } return nil } @@ -30,8 +24,6 @@ func (pm *ProcessModule) Stop() error { return nil } -var updatesPath string - func prep() error { if err := registerConfiguration(); err != nil { return err diff --git a/service/process/profile.go b/service/process/profile.go index e8c766ee..7ac4ed15 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -72,19 +72,20 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) { specialProfileID = profile.PortmasterProfileID default: // Check if this is another Portmaster component. - if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { - switch { - case strings.Contains(p.Path, "portmaster-app"): - specialProfileID = profile.PortmasterAppProfileID - case strings.Contains(p.Path, "portmaster-notifier"): - specialProfileID = profile.PortmasterNotifierProfileID - default: - // Unexpected binary from within the Portmaster updates directpry. - log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) - // TODO: Assign a fully restricted profile in the future when we are - // sure that we won't kill any of our own things. - } - } + // FIXME(vladimir): provide a better check for detecting filepath. Note there is exception on linux with portmaster ui. + // if updatesPath != "" && strings.HasPrefix(p.Path, updatesPath) { + // switch { + // case strings.Contains(p.Path, "portmaster-app"): + // specialProfileID = profile.PortmasterAppProfileID + // case strings.Contains(p.Path, "portmaster-notifier"): + // specialProfileID = profile.PortmasterNotifierProfileID + // default: + // // Unexpected binary from within the Portmaster updates directpry. + // log.Warningf("process: unexpected binary in the updates directory: %s", p.Path) + // // TODO: Assign a fully restricted profile in the future when we are + // // sure that we won't kill any of our own things. + // } + // } // Check if this is the system resolver. switch runtime.GOOS { case "windows": diff --git a/service/profile/module.go b/service/profile/module.go index 911ef99c..67873b8c 100644 --- a/service/profile/module.go +++ b/service/profile/module.go @@ -3,7 +3,6 @@ package profile import ( "errors" "fmt" - "os" "sync/atomic" "github.com/safing/portmaster/base/config" @@ -14,13 +13,9 @@ import ( _ "github.com/safing/portmaster/service/core/base" "github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/profile/binmeta" - "github.com/safing/portmaster/service/updates" ) -var ( - migrations = migration.New("core:migrations/profile") - updatesPath string -) +var migrations = migration.New("core:migrations/profile") // Events. const ( @@ -80,11 +75,6 @@ func prep() error { } func start() error { - updatesPath = updates.RootPath() - if updatesPath != "" { - updatesPath += string(os.PathSeparator) - } - if err := loadProfilesMetadata(); err != nil { if !errors.Is(err, database.ErrNotFound) { log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err) diff --git a/service/updates/api.go b/service/updates/api.go deleted file mode 100644 index 6c4dbf0c..00000000 --- a/service/updates/api.go +++ /dev/null @@ -1,161 +0,0 @@ -package updates - -import ( -// "bytes" -// "io" -// "net/http" -// "os" -// "path/filepath" -// "strings" - -// "github.com/ghodss/yaml" - -// "github.com/safing/portmaster/base/api" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -) - -const ( - apiPathCheckForUpdates = "updates/check" -) - -// func registerAPIEndpoints() error { -// if err := api.RegisterEndpoint(api.Endpoint{ -// Name: "Check for Updates", -// Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", -// Parameters: []api.Parameter{{ -// Method: http.MethodPost, -// Field: "download", -// Value: "", -// Description: "Force downloading and applying of all updates, regardless of auto-update settings.", -// }}, -// Path: apiPathCheckForUpdates, -// Write: api.PermitUser, -// ActionFunc: func(r *api.Request) (msg string, err error) { -// // Check if we should also download regardless of settings. -// downloadAll := r.URL.Query().Has("download") - -// // Trigger update task. -// err = TriggerUpdate(true, downloadAll) -// if err != nil { -// return "", err -// } - -// // Report how we triggered. -// if downloadAll { -// return "downloading all updates...", nil -// } -// return "checking for updates...", nil -// }, -// }); err != nil { -// return err -// } - -// if err := api.RegisterEndpoint(api.Endpoint{ -// Name: "Get Resource", -// Description: "Returns the requested resource from the udpate system", -// Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, -// Read: api.PermitUser, -// ReadMethod: http.MethodGet, -// HandlerFunc: func(w http.ResponseWriter, r *http.Request) { -// // Get identifier from URL. -// var identifier string -// if ar := api.GetAPIRequest(r); ar != nil { -// identifier = ar.URLVars["identifier"] -// } -// if identifier == "" { -// http.Error(w, "no resource speicified", http.StatusBadRequest) -// return -// } - -// // Get resource. -// resource, err := registry.GetFile(identifier) -// if err != nil { -// http.Error(w, err.Error(), http.StatusNotFound) -// return -// } - -// // Open file for reading. -// file, err := os.Open(resource.Path()) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// defer file.Close() //nolint:errcheck,gosec - -// // Assign file to reader -// var reader io.Reader = file - -// // Add version to header. -// w.Header().Set("Resource-Version", resource.Version()) - -// // Set Content-Type. -// contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) -// w.Header().Set("Content-Type", contentType) - -// // Check if the content type may be returned. -// accept := r.Header.Get("Accept") -// if accept != "" { -// mimeTypes := strings.Split(accept, ",") -// // First, clean mime types. -// for i, mimeType := range mimeTypes { -// mimeType = strings.TrimSpace(mimeType) -// mimeType, _, _ = strings.Cut(mimeType, ";") -// mimeTypes[i] = mimeType -// } -// // Second, check if we may return anything. -// var acceptsAny bool -// for _, mimeType := range mimeTypes { -// switch mimeType { -// case "*", "*/*": -// acceptsAny = true -// } -// } -// // Third, check if we can convert. -// if !acceptsAny { -// var converted bool -// sourceType, _, _ := strings.Cut(contentType, ";") -// findConvertiblePair: -// for _, mimeType := range mimeTypes { -// switch { -// case sourceType == "application/yaml" && mimeType == "application/json": -// yamlData, err := io.ReadAll(reader) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// jsonData, err := yaml.YAMLToJSON(yamlData) -// if err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } -// reader = bytes.NewReader(jsonData) -// converted = true -// break findConvertiblePair -// } -// } - -// // If we could not convert to acceptable format, return an error. -// if !converted { -// http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) -// return -// } -// } -// } - -// // Write file. -// w.WriteHeader(http.StatusOK) -// if r.Method != http.MethodHead { -// _, err = io.Copy(w, reader) -// if err != nil { -// log.Errorf("updates: failed to serve resource file: %s", err) -// return -// } -// } -// }, -// }); err != nil { -// return err -// } - -// return nil -// } diff --git a/service/updates/bundle.go b/service/updates/bundle.go index e590446d..904ae8fd 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -106,7 +107,7 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne if err != nil { return fmt.Errorf("failed to write to file %s: %w", destFilePath, err) } - + log.Debugf("updates: file copied from current version: %s", newArtifact.Filename) } break new } @@ -115,7 +116,7 @@ func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, ne return nil } -func (bundle Bundle) DownloadAndVerify(client *http.Client, dir string) { +func (bundle Bundle) DownloadAndVerify(ctx context.Context, client *http.Client, dir string) { // Make sure dir exists _ = os.MkdirAll(dir, defaultDirMode) @@ -130,7 +131,7 @@ func (bundle Bundle) DownloadAndVerify(client *http.Client, dir string) { } // Download artifact - err := processArtifact(client, artifact, filePath) + err := processArtifact(ctx, client, artifact, filePath) if err != nil { log.Errorf("updates: %s", err) } @@ -179,14 +180,15 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { return true, nil } -func processArtifact(client *http.Client, artifact Artifact, filePath string) error { +func processArtifact(ctx context.Context, client *http.Client, artifact Artifact, filePath string) error { providedHash, err := hex.DecodeString(artifact.SHA256) if err != nil || len(providedHash) != sha256.Size { return fmt.Errorf("invalid provided hash %s: %w", artifact.SHA256, err) } // Download - content, err := downloadFile(client, artifact.URLs) + log.Debugf("updates: downloading file: %s", artifact.Filename) + content, err := downloadFile(ctx, client, artifact.URLs) if err != nil { return fmt.Errorf("failed to download artifact: %w", err) } @@ -222,13 +224,23 @@ func processArtifact(client *http.Client, artifact Artifact, filePath string) er return fmt.Errorf("failed to rename file: %w", err) } + log.Infof("updates: file downloaded and verified: %s", artifact.Filename) + return nil } -func downloadFile(client *http.Client, urls []string) ([]byte, error) { +func downloadFile(ctx context.Context, client *http.Client, urls []string) ([]byte, error) { for _, url := range urls { // Try to make the request - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + if err != nil { + log.Warningf("failed to create GET request to %s: %s", url, err) + continue + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + resp, err := client.Do(req) if err != nil { log.Warningf("failed a get file request to: %s", err) continue diff --git a/service/updates/config.go b/service/updates/config.go deleted file mode 100644 index e563d557..00000000 --- a/service/updates/config.go +++ /dev/null @@ -1,178 +0,0 @@ -package updates - -import ( - "github.com/tevino/abool" - - "github.com/safing/portmaster/base/config" - // "github.com/safing/portmaster/base/log" - // "github.com/safing/portmaster/service/mgr" - // "github.com/safing/portmaster/service/updates/helper" -) - -const cfgDevModeKey = "core/devMode" - -var ( - releaseChannel config.StringOption - devMode config.BoolOption - enableSoftwareUpdates config.BoolOption - enableIntelUpdates config.BoolOption - - initialReleaseChannel string - previousReleaseChannel string - - softwareUpdatesCurrentlyEnabled bool - intelUpdatesCurrentlyEnabled bool - previousDevMode bool - forceCheck = abool.New() - forceDownload = abool.New() -) - -// func registerConfig() error { -// err := config.Register(&config.Option{ -// Name: "Release Channel", -// Key: helper.ReleaseChannelKey, -// Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`, -// OptType: config.OptTypeString, -// ExpertiseLevel: config.ExpertiseLevelExpert, -// ReleaseLevel: config.ReleaseLevelStable, -// RequiresRestart: true, -// DefaultValue: helper.ReleaseChannelStable, -// PossibleValues: []config.PossibleValue{ -// { -// Name: "Stable", -// Description: "Production releases.", -// Value: helper.ReleaseChannelStable, -// }, -// { -// Name: "Beta", -// Description: "Production releases for testing new features that may break and cause interruption.", -// Value: helper.ReleaseChannelBeta, -// }, -// { -// Name: "Support", -// Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.", -// Value: helper.ReleaseChannelSupport, -// }, -// { -// Name: "Staging", -// Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.", -// Value: helper.ReleaseChannelStaging, -// }, -// }, -// Annotations: config.Annotations{ -// config.DisplayOrderAnnotation: -4, -// config.DisplayHintAnnotation: config.DisplayHintOneOf, -// config.CategoryAnnotation: "Updates", -// }, -// }) -// if err != nil { -// return err -// } - -// err = config.Register(&config.Option{ -// 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, -// RequiresRestart: false, -// DefaultValue: true, -// Annotations: config.Annotations{ -// config.DisplayOrderAnnotation: -12, -// config.CategoryAnnotation: "Updates", -// }, -// }) -// if err != nil { -// 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 -// } - -// func initConfig() { -// releaseChannel = config.Concurrent.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable) -// initialReleaseChannel = releaseChannel() -// previousReleaseChannel = releaseChannel() - -// 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 updateRegistryConfig(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// changed := false - -// if enableSoftwareUpdates() != softwareUpdatesCurrentlyEnabled { -// softwareUpdatesCurrentlyEnabled = enableSoftwareUpdates() -// changed = true -// } - -// if enableIntelUpdates() != intelUpdatesCurrentlyEnabled { -// intelUpdatesCurrentlyEnabled = enableIntelUpdates() -// changed = true -// } - -// if devMode() != previousDevMode { -// registry.SetDevMode(devMode()) -// previousDevMode = devMode() -// changed = true -// } - -// 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.EventVersionsUpdated.Submit(struct{}{}) - -// if softwareUpdatesCurrentlyEnabled || intelUpdatesCurrentlyEnabled { -// module.states.Clear() -// if err := TriggerUpdate(true, false); err != nil { -// log.Warningf("updates: failed to trigger update: %s", err) -// } -// log.Infof("updates: automatic updates are now enabled") -// } else { -// log.Warningf("updates: automatic updates are now completely disabled") -// } -// } - -// return false, nil -// } diff --git a/service/updates/export.go b/service/updates/export.go deleted file mode 100644 index c736855c..00000000 --- a/service/updates/export.go +++ /dev/null @@ -1,237 +0,0 @@ -package updates - -// import ( -// "fmt" -// "sort" -// "sync" - -// "github.com/safing/portmaster/base/database/record" -// "github.com/safing/portmaster/base/info" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/base/utils/debug" -// "github.com/safing/portmaster/service/mgr" -// "github.com/safing/portmaster/service/updates/helper" -// ) - -// const ( -// // versionsDBKey is the database key for update version information. -// versionsDBKey = "core:status/versions" - -// // 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. -// type Versions struct { -// record.Base -// sync.Mutex - -// Core *info.Info -// Resources map[string]*updater.Resource -// Channel string -// Beta bool -// Staging bool -// } - -// // SimpleVersions holds simplified update versions and status information. -// type SimpleVersions struct { -// record.Base -// sync.Mutex - -// Build *info.Info -// Resources map[string]*SimplifiedResourceVersion -// Channel string -// } - -// // SimplifiedResourceVersion holds version information about one resource. -// 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 { -// return &Versions{ -// Core: info.GetInfo(), -// Resources: nil, -// Channel: initialReleaseChannel, -// Beta: initialReleaseChannel == helper.ReleaseChannelBeta, -// Staging: initialReleaseChannel == helper.ReleaseChannelStaging, -// } -// } - -// // GetSimpleVersions returns the simplified update versions and status information. -// func GetSimpleVersions() *SimpleVersions { -// // Fill base info. -// v := &SimpleVersions{ -// Build: info.GetInfo(), -// Resources: make(map[string]*SimplifiedResourceVersion), -// Channel: initialReleaseChannel, -// } - -// // Iterate through all versions and add version info. -// // for id, resource := range registry.Export() { -// // func() { -// // resource.Lock() -// // defer resource.Unlock() - -// // // Get current in-used or selected version. -// // var rv *updater.ResourceVersion -// // switch { -// // case resource.ActiveVersion != nil: -// // rv = resource.ActiveVersion -// // case resource.SelectedVersion != nil: -// // rv = resource.SelectedVersion -// // } - -// // // Get information from resource. -// // if rv != nil { -// // v.Resources[id] = &SimplifiedResourceVersion{ -// // Version: rv.VersionNumber, -// // } -// // } -// // }() -// // } - -// 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) -// } -// if err := GetSimpleVersions().save(); err != nil { -// log.Warningf("updates: failed to export version information: %s", err) -// } - -// // module.EventVersionsUpdated.AddCallback("export version status", export) -// return nil -// } - -// func (v *Versions) save() error { -// if !v.KeyIsSet() { -// v.SetKey(versionsDBKey) -// } -// return db.Put(v) -// } - -// func (v *SimpleVersions) save() error { -// if !v.KeyIsSet() { -// v.SetKey(simpleVersionsDBKey) -// } -// 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(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// // Export versions. -// if err := GetVersions().save(); err != nil { -// return false, err -// } -// if err := GetSimpleVersions().save(); err != nil { -// return false, err -// } -// // Export udpate state. -// if err := GetStateExport().save(); err != nil { -// return false, err -// } - -// return false, nil -// } - -// // AddToDebugInfo adds the update system status to the given debug.Info. -// func AddToDebugInfo(di *debug.Info) { -// // Get resources from registry. -// // resources := registry.Export() -// // platformPrefix := helper.PlatformIdentifier("") - -// // Collect data for debug info. -// var active, selected []string -// var activeCnt, totalCnt int -// // for id, r := range resources { -// // // Ignore resources for other platforms. -// // if !strings.HasPrefix(id, "all/") && !strings.HasPrefix(id, platformPrefix) { -// // continue -// // } - -// // totalCnt++ -// // if r.ActiveVersion != nil { -// // activeCnt++ -// // active = append(active, fmt.Sprintf("%s: %s", id, r.ActiveVersion.VersionNumber)) -// // } -// // if r.SelectedVersion != nil { -// // selected = append(selected, fmt.Sprintf("%s: %s", id, r.SelectedVersion.VersionNumber)) -// // } -// // } -// sort.Strings(active) -// sort.Strings(selected) - -// // Compile to one list. -// lines := make([]string, 0, len(active)+len(selected)+3) -// lines = append(lines, "Active:") -// lines = append(lines, active...) -// lines = append(lines, "") -// lines = append(lines, "Selected:") -// lines = append(lines, selected...) - -// // Add section. -// di.AddSection( -// fmt.Sprintf("Updates: %s (%d/%d)", initialReleaseChannel, activeCnt, totalCnt), -// debug.UseCodeSection|debug.AddContentLineBreaks, -// lines..., -// ) -// } diff --git a/service/updates/get.go b/service/updates/get.go deleted file mode 100644 index 75fc4c3e..00000000 --- a/service/updates/get.go +++ /dev/null @@ -1,65 +0,0 @@ -package updates - -// GetPlatformFile returns the latest platform specific file identified by the given identifier. -// func GetPlatformFile(identifier string) (*updater.File, error) { -// identifier = helper.PlatformIdentifier(identifier) - -// file, err := registry.GetFile(identifier) -// if err != nil { -// return nil, err -// } - -// module.EventVersionsUpdated.Submit(struct{}{}) -// return file, nil -// } - -// GetFile returns the latest generic file identified by the given identifier. -// func GetFile(identifier string) (*updater.File, error) { -// identifier = path.Join("all", identifier) - -// file, err := registry.GetFile(identifier) -// if err != nil { -// return nil, err -// } - -// module.EventVersionsUpdated.Submit(struct{}{}) -// 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 -// } - -// GetVersionWithFullID returns the selected generic version of the given full identifier. -// The returned resource version may not be modified. -// func GetVersionWithFullID(identifier string) (*updater.ResourceVersion, error) { -// rv, err := registry.GetVersion(identifier) -// if err != nil { -// return nil, err -// } - -// return rv, nil -// } diff --git a/service/updates/helper/electron.go b/service/updates/helper/electron.go deleted file mode 100644 index a0c10149..00000000 --- a/service/updates/helper/electron.go +++ /dev/null @@ -1,58 +0,0 @@ -package helper - -// import ( -// "errors" -// "fmt" -// "os" -// "path/filepath" -// "runtime" -// "strings" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/service/updates/registry" -// ) - -// var pmElectronUpdate *registry.File - -// const suidBitWarning = `Failed to set SUID permissions for chrome-sandbox. This is required for Linux kernel versions that do not have unprivileged user namespaces (CONFIG_USER_NS_UNPRIVILEGED) enabled. If you're running and up-to-date distribution kernel you can likely ignore this warning. If you encounter issue starting the user interface please either update your kernel or set the SUID bit (mode 0%0o) on %s` - -// // EnsureChromeSandboxPermissions makes sure the chrome-sandbox distributed -// // by our app-electron package has the SUID bit set on systems that do not -// // allow unprivileged CLONE_NEWUSER (clone(3)). -// // On non-linux systems or systems that have kernel.unprivileged_userns_clone -// // set to 1 EnsureChromeSandboPermissions is a NO-OP. -// func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error { -// if runtime.GOOS != "linux" { -// return nil -// } - -// if pmElectronUpdate != nil && !pmElectronUpdate.UpgradeAvailable() { -// return nil -// } - -// identifier := PlatformIdentifier("app/portmaster-app.zip") - -// var err error -// pmElectronUpdate, err = reg.GetFile(identifier) -// if err != nil { -// if errors.Is(err, updater.ErrNotAvailableLocally) { -// return nil -// } -// return fmt.Errorf("failed to get file: %w", err) -// } - -// unpackedPath := strings.TrimSuffix( -// pmElectronUpdate.Path(), -// filepath.Ext(pmElectronUpdate.Path()), -// ) -// sandboxFile := filepath.Join(unpackedPath, "chrome-sandbox") -// if err := os.Chmod(sandboxFile, 0o0755|os.ModeSetuid); err != nil { -// log.Errorf(suidBitWarning, 0o0755|os.ModeSetuid, sandboxFile) - -// return fmt.Errorf("failed to chmod: %w", err) -// } -// log.Debugf("updates: fixed SUID permission for chrome-sandbox") - -// return nil -// } diff --git a/service/updates/helper/indexes.go b/service/updates/helper/indexes.go deleted file mode 100644 index 7b9e671e..00000000 --- a/service/updates/helper/indexes.go +++ /dev/null @@ -1,136 +0,0 @@ -package helper - -// import ( -// "errors" -// "fmt" -// "io/fs" -// "os" -// "path/filepath" - -// "github.com/safing/jess/filesig" -// "github.com/safing/portmaster/base/updater" -// ) - -// // Release Channel Configuration Keys. -// const ( -// ReleaseChannelKey = "core/releaseChannel" -// ReleaseChannelJSONKey = "core.releaseChannel" -// ) - -// // Release Channels. -// const ( -// ReleaseChannelStable = "stable" -// ReleaseChannelBeta = "beta" -// ReleaseChannelStaging = "staging" -// ReleaseChannelSupport = "support" -// ) - -// const jsonSuffix = ".json" - -// // 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, -// autoDownload bool, -// autoDownloadIntel bool, -// ) (warning error) { -// usePreReleases := false - -// // Be reminded that the order is important, as indexes added later will -// // override the current release from earlier indexes. - -// // Reset indexes before adding them (again). -// registry.ResetIndexes() - -// // 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", -// AutoDownload: autoDownloadIntel, -// }) - -// // Always add the stable index as a base. -// registry.AddIndex(updater.Index{ -// Path: ReleaseChannelStable + jsonSuffix, -// AutoDownload: autoDownload, -// }) - -// // Add beta index if in beta or staging channel. -// indexPath := ReleaseChannelBeta + jsonSuffix -// if releaseChannel == ReleaseChannelBeta || -// releaseChannel == ReleaseChannelStaging || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// PreRelease: true, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Add staging index if in staging channel. -// indexPath = ReleaseChannelStaging + jsonSuffix -// if releaseChannel == ReleaseChannelStaging || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// PreRelease: true, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Add support index if in support channel. -// indexPath = ReleaseChannelSupport + jsonSuffix -// if releaseChannel == ReleaseChannelSupport || -// (releaseChannel == "" && indexExists(registry, indexPath)) { -// registry.AddIndex(updater.Index{ -// Path: indexPath, -// AutoDownload: autoDownload, -// }) -// usePreReleases = true -// } else if deleteUnusedIndexes { -// err := deleteIndex(registry, indexPath) -// if err != nil { -// warning = fmt.Errorf("failed to delete unused index %s: %w", indexPath, err) -// } -// } - -// // Set pre-release usage. -// registry.SetUsePreReleases(usePreReleases) - -// return warning -// } - -// func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { -// _, err := os.Stat(filepath.Join(registry.StorageDir().Path, indexPath)) -// return err == nil -// } - -// func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { -// // Remove index itself. -// err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) -// if err != nil && !errors.Is(err, fs.ErrNotExist) { -// return err -// } - -// // Remove any accompanying signature. -// err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) -// if err != nil && !errors.Is(err, fs.ErrNotExist) { -// return err -// } - -// return nil -// } diff --git a/service/updates/helper/signing.go b/service/updates/helper/signing.go deleted file mode 100644 index 9af98699..00000000 --- a/service/updates/helper/signing.go +++ /dev/null @@ -1,42 +0,0 @@ -package helper - -// import ( -// "github.com/safing/jess" -// "github.com/safing/portmaster/base/updater" -// ) - -// var ( -// // VerificationConfig holds the complete verification configuration for the registry. -// VerificationConfig = map[string]*updater.VerificationOptions{ -// "": { // Default. -// TrustStore: BinarySigningTrustStore, -// DownloadPolicy: updater.SignaturePolicyRequire, -// DiskLoadPolicy: updater.SignaturePolicyWarn, -// }, -// "all/intel/": nil, // Disable until IntelHub supports signing. -// } - -// // BinarySigningKeys holds the signing keys in text format. -// BinarySigningKeys = []string{ -// // Safing Code Signing Key #1 -// "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", -// // Safing Code Signing Key #2 -// "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", -// } - -// // BinarySigningTrustStore is an in-memory trust store with the signing keys. -// BinarySigningTrustStore = jess.NewMemTrustStore() -// ) - -// func init() { -// for _, signingKey := range BinarySigningKeys { -// rcpt, err := jess.RecipientFromTextFormat(signingKey) -// if err != nil { -// panic(err) -// } -// err = BinarySigningTrustStore.StoreSignet(rcpt) -// if err != nil { -// panic(err) -// } -// } -// } diff --git a/service/updates/helper/updates.go b/service/updates/helper/updates.go deleted file mode 100644 index 135c1222..00000000 --- a/service/updates/helper/updates.go +++ /dev/null @@ -1,95 +0,0 @@ -package helper - -// import ( -// "fmt" -// "runtime" - -// "github.com/tevino/abool" -// ) - -// const onWindows = runtime.GOOS == "windows" - -// var intelOnly = abool.New() - -// // IntelOnly specifies that only intel data is mandatory. -// func IntelOnly() { -// intelOnly.Set() -// } - -// // PlatformIdentifier converts identifier for the current platform. -// func PlatformIdentifier(identifier string) string { -// // From https://golang.org/pkg/runtime/#GOARCH -// // GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on. -// // GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on. -// return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier) -// } - -// // MandatoryUpdates returns mandatory updates that should be loaded on install -// // or reset. -// func MandatoryUpdates() (identifiers []string) { -// // Intel -// identifiers = append( -// identifiers, - -// // Filter lists data -// "all/intel/lists/index.dsd", -// "all/intel/lists/base.dsdl", -// "all/intel/lists/intermediate.dsdl", -// "all/intel/lists/urgent.dsdl", - -// // Geo IP data -// "all/intel/geoip/geoipv4.mmdb.gz", -// "all/intel/geoip/geoipv6.mmdb.gz", -// ) - -// // Stop here if we only want intel data. -// if intelOnly.IsSet() { -// return identifiers -// } - -// // Binaries -// if onWindows { -// identifiers = append( -// identifiers, -// PlatformIdentifier("core/portmaster-core.exe"), -// PlatformIdentifier("kext/portmaster-kext.sys"), -// PlatformIdentifier("kext/portmaster-kext.pdb"), -// PlatformIdentifier("start/portmaster-start.exe"), -// PlatformIdentifier("notifier/portmaster-notifier.exe"), -// PlatformIdentifier("notifier/portmaster-wintoast.dll"), -// PlatformIdentifier("app2/portmaster-app.zip"), -// ) -// } else { -// identifiers = append( -// identifiers, -// PlatformIdentifier("core/portmaster-core"), -// PlatformIdentifier("start/portmaster-start"), -// PlatformIdentifier("notifier/portmaster-notifier"), -// PlatformIdentifier("app2/portmaster-app"), -// ) -// } - -// // Components, Assets and Data -// identifiers = append( -// identifiers, - -// // User interface components -// PlatformIdentifier("app/portmaster-app.zip"), -// "all/ui/modules/portmaster.zip", -// "all/ui/modules/assets.zip", -// ) - -// return identifiers -// } - -// // AutoUnpackUpdates returns assets that need unpacking. -// func AutoUnpackUpdates() []string { -// if intelOnly.IsSet() { -// return []string{} -// } - -// return []string{ -// PlatformIdentifier("app/portmaster-app.zip"), -// PlatformIdentifier("app2/portmaster-app.zip"), -// } -// } diff --git a/service/updates/index.go b/service/updates/index.go index b753450c..c9a5f29d 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -1,6 +1,7 @@ package updates import ( + "context" "fmt" "io" "net/http" @@ -18,13 +19,14 @@ type UpdateIndex struct { IndexURLs []string IndexFile string AutoApply bool + NeedsRestart bool } -func (ui *UpdateIndex) DownloadIndexFile(client *http.Client) (err error) { +func (ui *UpdateIndex) DownloadIndexFile(ctx context.Context, client *http.Client) (err error) { // Make sure dir exists _ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode) for _, url := range ui.IndexURLs { - err = ui.downloadIndexFileFromURL(client, url) + err = ui.downloadIndexFileFromURL(ctx, client, url) if err != nil { log.Warningf("updates: failed while downloading index file %s", err) continue @@ -36,9 +38,18 @@ func (ui *UpdateIndex) DownloadIndexFile(client *http.Client) (err error) { return } -func (ui *UpdateIndex) downloadIndexFileFromURL(client *http.Client, url string) error { +func (ui *UpdateIndex) downloadIndexFileFromURL(ctx context.Context, client *http.Client, url string) error { // Request the index file - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) + if err != nil { + return fmt.Errorf("failed to create GET request to %s: %w", url, err) + } + if UserAgent != "" { + req.Header.Set("User-Agent", UserAgent) + } + + // Perform request + resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed GET request to %s: %w", url, err) } diff --git a/service/updates/main.go b/service/updates/main.go index f9e68b8b..de15a98b 100644 --- a/service/updates/main.go +++ b/service/updates/main.go @@ -1,70 +1 @@ package updates - -import ( - "fmt" - "runtime" - "time" - - "github.com/safing/portmaster/base/database" -) - -const ( - onWindows = runtime.GOOS == "windows" - - enableSoftwareUpdatesKey = "core/automaticUpdates" - enableIntelUpdatesKey = "core/automaticIntelUpdates" - - // 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" -) - -var ( - userAgentFromFlag string - updateServerFromFlag string - - db = database.NewInterface(&database.Options{ - Local: true, - Internal: true, - }) - - // 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. - UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH) -) - -const ( - updateTaskRepeatDuration = 1 * time.Hour -) - -func stop() error { - // if registry != nil { - // err := registry.Cleanup() - // if err != nil { - // log.Warningf("updates: failed to clean up registry: %s", err) - // } - // } - - return nil -} - -// RootPath returns the root path used for storing updates. -func RootPath() string { - // if !module.Online() { - // return "" - // } - - // return registry.StorageDir().Path - return "" -} diff --git a/service/updates/module.go b/service/updates/module.go index 99c110da..5caf2ab4 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -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 { diff --git a/service/updates/notify.go b/service/updates/notify.go deleted file mode 100644 index 1a539f7a..00000000 --- a/service/updates/notify.go +++ /dev/null @@ -1,176 +0,0 @@ -package updates - -import ( - "sync/atomic" - "time" -) - -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 (u *Updates) notificationsEnabled() bool { - return u.instance.Notifications() != nil -} - -// func notifyUpdateSuccess(force bool) { -// if !module.notificationsEnabled() { -// return -// } - -// updateFailedCnt.Store(0) -// module.states.Clear() -// 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) < 5*time.Second: -// // Show notification if we downloaded something within the last minute. -// flavor = updateSuccessDownloaded -// case force: -// // 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(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: "Portmaster Is Up-To-Date", -// Message: "Portmaster successfully checked for updates. Everything is up to date.\n\n" + getUpdatingInfoMsg(), -// Expires: time.Now().Add(1 * time.Minute).Unix(), -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// }, -// }) - -// case updateSuccessPending: -// msg := fmt.Sprintf( -// `%d updates are available for download: - -// - %s - -// Press "Download Now" to download and automatically apply all pending updates. You will be notified of important updates that need restarting.`, -// len(updateState.PendingDownload), -// strings.Join(updateState.PendingDownload, "\n- "), -// ) - -// notifications.Notify(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: fmt.Sprintf("%d Updates Available", len(updateState.PendingDownload)), -// Message: msg, -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// { -// ID: "download", -// Text: "Download Now", -// Type: notifications.ActionTypeWebhook, -// Payload: ¬ifications.ActionTypeWebhookPayload{ -// URL: apiPathCheckForUpdates + "?download", -// ResultAction: "display", -// }, -// }, -// }, -// }) - -// case updateSuccessDownloaded: -// msg := fmt.Sprintf( -// `%d updates were downloaded and applied: - -// - %s - -// %s -// `, -// len(updateState.LastDownload), -// strings.Join(updateState.LastDownload, "\n- "), -// getUpdatingInfoMsg(), -// ) - -// notifications.Notify(¬ifications.Notification{ -// EventID: updateSuccess, -// Type: notifications.Info, -// Title: fmt.Sprintf("%d Updates Applied", len(updateState.LastDownload)), -// Message: msg, -// Expires: time.Now().Add(1 * time.Minute).Unix(), -// AvailableActions: []*notifications.Action{ -// { -// ID: "ack", -// Text: "OK", -// }, -// }, -// }) - -// } -// } - -func getUpdatingInfoMsg() string { - switch { - case enableSoftwareUpdates() && enableIntelUpdates(): - return "You will be notified of important updates that need restarting." - case enableIntelUpdates(): - return "Automatic software updates are disabled, but you will be notified when a new software update is ready to be downloaded and applied." - default: - return "Automatic software updates are disabled. Please check for updates regularly yourself." - } -} - -// func notifyUpdateCheckFailed(force bool, err error) { -// if !module.notificationsEnabled() { -// return -// } - -// failedCnt := updateFailedCnt.Add(1) -// lastSuccess := registry.GetState().Updates.LastSuccessAt - -// switch { -// case force: -// // Always show notification if update was manually triggered. -// case failedCnt < failedUpdateNotifyCountThreshold: -// // Not failed often enough for notification. -// return -// case lastSuccess == nil: -// // No recorded successful update. -// 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: ¬ifications.ActionTypeWebhookPayload{ -// URL: apiPathCheckForUpdates, -// ResultAction: "display", -// }, -// }, -// ).SyncWithState(module.states) -// } diff --git a/service/updates/os_integration_default.go b/service/updates/os_integration_default.go deleted file mode 100644 index b817c351..00000000 --- a/service/updates/os_integration_default.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !linux -// +build !linux - -package updates - -func upgradeSystemIntegration() error { - return nil -} diff --git a/service/updates/os_integration_linux.go b/service/updates/os_integration_linux.go deleted file mode 100644 index cd1b6137..00000000 --- a/service/updates/os_integration_linux.go +++ /dev/null @@ -1,201 +0,0 @@ -package updates - -// import ( -// "crypto/sha256" -// _ "embed" -// "encoding/hex" -// "errors" -// "fmt" -// "io/fs" -// "os" -// "path/filepath" - -// "github.com/tevino/abool" -// "golang.org/x/exp/slices" - -// "github.com/safing/portmaster/base/dataroot" -// "github.com/safing/portmaster/base/log" -// ) - -// var ( -// portmasterCoreServiceFilePath = "portmaster.service" -// portmasterNotifierServiceFilePath = "portmaster_notifier.desktop" -// backupExtension = ".backup" - -// //go:embed assets/portmaster.service -// currentPortmasterCoreServiceFile []byte - -// checkedSystemIntegration = abool.New() - -// // ErrRequiresManualUpgrade is returned when a system integration file requires a manual upgrade. -// ErrRequiresManualUpgrade = errors.New("requires a manual upgrade") -// ) - -// func upgradeSystemIntegration() { -// // Check if we already checked the system integration. -// if !checkedSystemIntegration.SetToIf(false, true) { -// return -// } - -// // Upgrade portmaster core systemd service. -// err := upgradeSystemIntegrationFile( -// "portmaster core systemd service", -// filepath.Join(dataroot.Root().Path, portmasterCoreServiceFilePath), -// 0o0600, -// currentPortmasterCoreServiceFile, -// []string{ -// "bc26dd37e6953af018ad3676ee77570070e075f2b9f5df6fa59d65651a481468", // Commit 19c76c7 on 2022-01-25 -// "cc0cb49324dfe11577e8c066dd95cc03d745b50b2153f32f74ca35234c3e8cb5", // Commit ef479e5 on 2022-01-24 -// "d08a3b5f3aee351f8e120e6e2e0a089964b94c9e9d0a9e5fa822e60880e315fd", // Commit b64735e on 2021-12-07 -// }, -// ) -// if err != nil { -// log.Warningf("updates: %s", err) -// return -// } - -// // Upgrade portmaster notifier systemd user service. -// // Permissions only! -// err = upgradeSystemIntegrationFile( -// "portmaster notifier systemd user service", -// filepath.Join(dataroot.Root().Path, portmasterNotifierServiceFilePath), -// 0o0644, -// nil, // Do not update contents. -// nil, // Do not update contents. -// ) -// if err != nil { -// log.Warningf("updates: %s", err) -// return -// } -// } - -// // upgradeSystemIntegrationFile upgrades the file contents and permissions. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// // The supplied hashes must be sha256 hex-encoded. -// func upgradeSystemIntegrationFile( -// name string, -// filePath string, -// fileMode fs.FileMode, -// fileData []byte, -// permittedUpgradeHashes []string, -// ) error { -// // Upgrade file contents. -// if len(fileData) > 0 { -// if err := upgradeSystemIntegrationFileContents(name, filePath, fileData, permittedUpgradeHashes); err != nil { -// return err -// } -// } - -// // Upgrade file permissions. -// if fileMode != 0 { -// if err := upgradeSystemIntegrationFilePermissions(name, filePath, fileMode); err != nil { -// return err -// } -// } - -// return nil -// } - -// // upgradeSystemIntegrationFileContents upgrades the file contents. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// // The supplied hashes must be sha256 hex-encoded. -// func upgradeSystemIntegrationFileContents( -// name string, -// filePath string, -// fileData []byte, -// permittedUpgradeHashes []string, -// ) error { -// // Read existing file. -// existingFileData, err := os.ReadFile(filePath) -// if err != nil { -// if errors.Is(err, os.ErrNotExist) { -// return nil -// } -// return fmt.Errorf("failed to read %s at %s: %w", name, filePath, err) -// } - -// // Check if file is already the current version. -// existingSum := sha256.Sum256(existingFileData) -// existingHexSum := hex.EncodeToString(existingSum[:]) -// currentSum := sha256.Sum256(fileData) -// currentHexSum := hex.EncodeToString(currentSum[:]) -// if existingHexSum == currentHexSum { -// log.Debugf("updates: %s at %s is up to date", name, filePath) -// return nil -// } - -// // Check if we are allowed to upgrade from the existing file. -// if !slices.Contains[[]string, string](permittedUpgradeHashes, existingHexSum) { -// return fmt.Errorf("%s at %s (sha256:%s) %w, as it is not a previously published version and cannot be automatically upgraded - try installing again", name, filePath, existingHexSum, ErrRequiresManualUpgrade) -// } - -// // Start with upgrade! - -// // Make backup of existing file. -// err = CopyFile(filePath, filePath+backupExtension) -// if err != nil { -// return fmt.Errorf( -// "failed to create backup of %s from %s to %s: %w", -// name, -// filePath, -// filePath+backupExtension, -// err, -// ) -// } - -// // Open destination file for writing. -// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, filePath) -// // if err != nil { -// // return fmt.Errorf("failed to create tmp file to update %s at %s: %w", name, filePath, err) -// // } -// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // // Write file. -// // _, err = io.Copy(atomicDstFile, bytes.NewReader(fileData)) -// // if err != nil { -// // return err -// // } - -// // // Finalize file. -// // err = atomicDstFile.CloseAtomicallyReplace() -// // if err != nil { -// // return fmt.Errorf("failed to finalize update of %s at %s: %w", name, filePath, err) -// // } - -// log.Warningf("updates: %s at %s was upgraded to %s - a reboot may be required", name, filePath, currentHexSum) -// return nil -// } - -// // upgradeSystemIntegrationFilePermissions upgrades the file permissions. -// // System integration files are not necessarily present and may also be -// // edited by third parties, such as the OS itself or other installers. -// func upgradeSystemIntegrationFilePermissions( -// name string, -// filePath string, -// fileMode fs.FileMode, -// ) error { -// // Get current file permissions. -// stat, err := os.Stat(filePath) -// if err != nil { -// if errors.Is(err, os.ErrNotExist) { -// return nil -// } -// return fmt.Errorf("failed to read %s file metadata at %s: %w", name, filePath, err) -// } - -// // If permissions are as expected, do nothing. -// if stat.Mode().Perm() == fileMode { -// return nil -// } - -// // Otherwise, set correct permissions. -// err = os.Chmod(filePath, fileMode) -// if err != nil { -// return fmt.Errorf("failed to update %s file permissions at %s: %w", name, filePath, err) -// } - -// log.Warningf("updates: %s file permissions at %s updated to %v", name, filePath, fileMode) -// return nil -// } diff --git a/service/updates/state.go b/service/updates/state.go deleted file mode 100644 index ef104308..00000000 --- a/service/updates/state.go +++ /dev/null @@ -1,49 +0,0 @@ -package updates - -// import ( -// "github.com/safing/portmaster/base/database/record" -// "github.com/safing/portmaster/base/runtime" -// "github.com/safing/portmaster/base/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 -// } diff --git a/service/updates/updater.go b/service/updates/updater.go index 4a0358ec..f76ed2b9 100644 --- a/service/updates/updater.go +++ b/service/updates/updater.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "github.com/safing/portmaster/base/log" @@ -29,6 +30,7 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { } // Move current version files into purge folder. + log.Debugf("updates: removing the old version") for _, file := range files { currentFilepath := filepath.Join(updateIndex.Directory, file.Name()) purgePath := filepath.Join(updateIndex.PurgeDirectory, file.Name()) @@ -39,6 +41,7 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { } // Move the new index file + log.Debugf("updates: installing the new version") indexFile := filepath.Join(updateIndex.DownloadDirectory, updateIndex.IndexFile) newIndexFile := filepath.Join(updateIndex.Directory, updateIndex.IndexFile) err = os.Rename(indexFile, newIndexFile) @@ -52,9 +55,27 @@ func switchFolders(updateIndex UpdateIndex, newBundle Bundle) error { toFilepath := filepath.Join(updateIndex.Directory, artifact.Filename) err = os.Rename(fromFilepath, toFilepath) if err != nil { - return fmt.Errorf("failed to move file %s: %w", fromFilepath, err) + log.Errorf("failed to move file %s: %s", fromFilepath, err) + } else { + log.Debugf("updates: %s moved", artifact.Filename) + } + + // Special case for linux. + // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` + // After an update the original binary should be deleted and replaced by symlink + // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` + if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { + err = makeSymlinkForUI(updateIndex.Directory) + if err != nil { + log.Errorf("failed to create symlink for the ui: %s", err) + } else { + log.Infof("ui symlink successfully created") + } } } + + log.Debugf("updates: update complete") + return nil } @@ -76,3 +97,11 @@ func deleteUnfinishedDownloads(rootDir string) error { } return nil } + +func makeSymlinkForUI(directory string) error { + err := os.Symlink(filepath.Join(directory, "portmaster"), "/usr/bin/portmaster") + if err != nil { + return fmt.Errorf("failed to create symlink: %w", err) + } + return nil +} diff --git a/service/updates/upgrader.go b/service/updates/upgrader.go deleted file mode 100644 index 334ebe88..00000000 --- a/service/updates/upgrader.go +++ /dev/null @@ -1,403 +0,0 @@ -package updates - -// import ( -// "context" -// "fmt" -// "os" -// "os/exec" -// "path/filepath" -// "regexp" -// "strings" -// "time" - -// processInfo "github.com/shirou/gopsutil/process" -// "github.com/tevino/abool" - -// "github.com/safing/portmaster/base/dataroot" -// "github.com/safing/portmaster/base/info" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/notifications" -// "github.com/safing/portmaster/base/rng" -// "github.com/safing/portmaster/base/updater" -// "github.com/safing/portmaster/service/mgr" -// ) - -// const ( -// upgradedSuffix = "-upgraded" -// exeExt = ".exe" -// ) - -// var ( -// upgraderActive = abool.NewBool(false) - -// pmCtrlUpdate *updater.File -// pmCoreUpdate *updater.File - -// spnHubUpdate *updater.File - -// rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`) -// ) - -// func initUpgrader() error { -// // module.EventResourcesUpdated.AddCallback("run upgrades", upgrader) -// return nil -// } - -// func upgrader(m *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) { -// // Lock runs, but discard additional runs. -// if !upgraderActive.SetToIf(false, true) { -// return false, nil -// } -// defer upgraderActive.SetTo(false) - -// // Upgrade portmaster-start. -// err = upgradePortmasterStart() -// if err != nil { -// log.Warningf("updates: failed to upgrade portmaster-start: %s", err) -// } - -// // Upgrade based on binary. -// binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0] -// switch binBaseName { -// case "portmaster-core": -// // Notify about upgrade. -// if err := upgradeCoreNotify(); err != nil { -// log.Warningf("updates: failed to notify about core upgrade: %s", err) -// } - -// // Fix chrome sandbox permissions. -// // if err := helper.EnsureChromeSandboxPermissions(registry); err != nil { -// // log.Warningf("updates: failed to handle electron upgrade: %s", err) -// // } - -// // Upgrade system integration. -// upgradeSystemIntegration() - -// case "spn-hub": -// // Trigger upgrade procedure. -// if err := upgradeHub(); err != nil { -// log.Warningf("updates: failed to initiate hub upgrade: %s", err) -// } -// } - -// return false, nil -// } - -// func upgradeCoreNotify() error { -// if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() { -// return nil -// } - -// // make identifier -// identifier := "core/portmaster-core" // identifier, use forward slash! -// if onWindows { -// identifier += exeExt -// } - -// // get newest portmaster-core -// // newFile, err := GetPlatformFile(identifier) -// // if err != nil { -// // return err -// // } -// // pmCoreUpdate = newFile - -// // check for new version -// if info.VersionNumber() != pmCoreUpdate.Version() { -// n := notifications.Notify(¬ifications.Notification{ -// EventID: "updates:core-update-available", -// Type: notifications.Info, -// Title: fmt.Sprintf( -// "Portmaster Update v%s Is Ready!", -// pmCoreUpdate.Version(), -// ), -// Category: "Core", -// Message: fmt.Sprintf( -// `A new Portmaster version is ready to go! Restart the Portmaster to upgrade to %s.`, -// pmCoreUpdate.Version(), -// ), -// ShowOnSystem: true, -// AvailableActions: []*notifications.Action{ -// // TODO: Use special UI action in order to reload UI on restart. -// { -// ID: "restart", -// Text: "Restart", -// }, -// { -// ID: "later", -// Text: "Not now", -// }, -// }, -// }) -// n.SetActionFunction(upgradeCoreNotifyActionHandler) - -// log.Debugf("updates: new portmaster version available, sending notification to user") -// } - -// return nil -// } - -// func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error { -// switch n.SelectedActionID { -// case "restart": -// log.Infof("updates: user triggered restart via core update notification") -// RestartNow() -// case "later": -// n.Delete() -// } - -// return nil -// } - -// func upgradeHub() error { -// if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() { -// return nil -// } - -// // Make identifier for getting file from updater. -// identifier := "hub/spn-hub" // identifier, use forward slash! -// if onWindows { -// identifier += exeExt -// } - -// // Get newest spn-hub file. -// // newFile, err := GetPlatformFile(identifier) -// // if err != nil { -// // return err -// // } -// // spnHubUpdate = newFile - -// // Check if the new version is different. -// if info.GetInfo().Version != spnHubUpdate.Version() { -// // Get random delay with up to three hours. -// delayMinutes, err := rng.Number(3 * 60) -// if err != nil { -// return err -// } - -// // Delay restart for at least one hour for preparations. -// DelayedRestart(time.Duration(delayMinutes+60) * time.Minute) - -// // Increase update checks in order to detect aborts better. -// // if !disableTaskSchedule { -// // module.updateBinaryWorkerMgr.Repeat(10 * time.Minute) -// // } -// } else { -// AbortRestart() - -// // Set update task schedule back to normal. -// // if !disableTaskSchedule { -// // module.updateBinaryWorkerMgr.Repeat(updateTaskRepeatDuration) -// // } -// } - -// return nil -// } - -// func upgradePortmasterStart() error { -// filename := "portmaster-start" -// if onWindows { -// filename += exeExt -// } - -// // check if we can upgrade -// if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() { -// // get newest portmaster-start -// // newFile, err := GetPlatformFile("start/" + filename) // identifier, use forward slash! -// // if err != nil { -// // return err -// // } -// // pmCtrlUpdate = newFile -// } else { -// return nil -// } - -// // update portmaster-start in data root -// rootPmStartPath := filepath.Join(dataroot.Root().Path, filename) -// err := upgradeBinary(rootPmStartPath, pmCtrlUpdate) -// if err != nil { -// return err -// } - -// return nil -// } - -// func warnOnIncorrectParentPath() { -// expectedFileName := "portmaster-start" -// if onWindows { -// expectedFileName += exeExt -// } - -// // upgrade parent process, if it's portmaster-start -// parent, err := processInfo.NewProcess(int32(os.Getppid())) -// if err != nil { -// log.Tracef("could not get parent process: %s", err) -// return -// } -// parentName, err := parent.Name() -// if err != nil { -// log.Tracef("could not get parent process name: %s", err) -// return -// } -// if parentName != expectedFileName { -// // Only warn about this if not in dev mode. -// if !devMode() { -// log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName) -// } - -// // TODO(ppacher): once we released a new installer and folks had time -// // to update we should send a module warning/hint to the -// // UI notifying the user that he's still using portmaster-control. -// return -// } - -// // parentPath, err := parent.Exe() -// // if err != nil { -// // log.Tracef("could not get parent process path: %s", err) -// // return -// // } - -// // absPath, err := filepath.Abs(parentPath) -// // if err != nil { -// // log.Tracef("could not get absolut parent process path: %s", err) -// // return -// // } - -// // root := filepath.Dir(registry.StorageDir().Path) -// // if !strings.HasPrefix(absPath, root) { -// // log.Warningf("detected unexpected path %s for portmaster-start", absPath) -// // notifications.NotifyWarn( -// // "updates:unsupported-parent", -// // "Unsupported Launcher", -// // fmt.Sprintf( -// // "The Portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", -// // expectedFileName, -// // absPath, -// // filepath.Join(root, expectedFileName), -// // ), -// // ) -// // } -// } - -// func upgradeBinary(fileToUpgrade string, file *updater.File) error { -// fileExists := false -// _, err := os.Stat(fileToUpgrade) -// if err == nil { -// // file exists and is accessible -// fileExists = true -// } - -// if fileExists { -// // get current version -// var currentVersion string -// cmd := exec.Command(fileToUpgrade, "version", "--short") -// out, err := cmd.Output() -// if err == nil { -// // abort if version matches -// currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") -// if currentVersion == file.Version() { -// log.Debugf("updates: %s is already v%s", fileToUpgrade, file.Version()) -// // already up to date! -// return nil -// } -// } else { -// log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) -// currentVersion = "0.0.0" -// } - -// // test currentVersion for sanity -// if !rawVersionRegex.MatchString(currentVersion) { -// log.Debugf("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) -// } - -// // try removing old version -// err = os.Remove(fileToUpgrade) -// if err != nil { -// // ensure tmp dir is here -// // err = registry.TmpDir().Ensure() -// // if err != nil { -// // return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err) -// // } - -// // maybe we're on windows and it's in use, try moving -// // err = os.Rename(fileToUpgrade, filepath.Join( -// // registry.TmpDir().Path, -// // fmt.Sprintf( -// // "%s-%d%s", -// // filepath.Base(fileToUpgrade), -// // time.Now().UTC().Unix(), -// // upgradedSuffix, -// // ), -// // )) -// // if err != nil { -// // return fmt.Errorf("unable to move file that needs upgrade: %w", err) -// // } -// } -// } - -// // copy upgrade -// err = CopyFile(file.Path(), fileToUpgrade) -// if err != nil { -// // try again -// time.Sleep(1 * time.Second) -// err = CopyFile(file.Path(), fileToUpgrade) -// if err != nil { -// return err -// } -// } - -// // check permissions -// if !onWindows { -// info, err := os.Stat(fileToUpgrade) -// if err != nil { -// return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err) -// } -// if info.Mode() != 0o0755 { -// err := os.Chmod(fileToUpgrade, 0o0755) //nolint:gosec // Set execute permissions. -// if err != nil { -// return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err) -// } -// } -// } - -// log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version()) -// return nil -// } - -// // CopyFile atomically copies a file using the update registry's tmp dir. -// func CopyFile(srcPath, dstPath string) error { -// // check tmp dir -// // err := registry.TmpDir().Ensure() -// // if err != nil { -// // return fmt.Errorf("could not prepare tmp directory for copying file: %w", err) -// // } - -// // open file for writing -// // atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath) -// // if err != nil { -// // return fmt.Errorf("could not create temp file for atomic copy: %w", err) -// // } -// // defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // // open source -// // srcFile, err := os.Open(srcPath) -// // if err != nil { -// // return err -// // } -// // defer func() { -// // _ = srcFile.Close() -// // }() - -// // // copy data -// // _, err = io.Copy(atomicDstFile, srcFile) -// // if err != nil { -// // return err -// // } - -// // // finalize file -// // err = atomicDstFile.CloseAtomicallyReplace() -// // if err != nil { -// // return fmt.Errorf("updates: failed to finalize copy to file %s: %w", dstPath, err) -// // } - -// return nil -// }