mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Adapt updates package to new updater module in portbase, clean up
This commit is contained in:
parent
92ccb36952
commit
bfde6cb044
21 changed files with 455 additions and 1210 deletions
63
updates/config.go
Normal file
63
updates/config.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
)
|
||||
|
||||
var (
|
||||
releaseChannel config.StringOption
|
||||
devMode config.BoolOption
|
||||
|
||||
previousReleaseChannel string
|
||||
previousDevMode bool
|
||||
)
|
||||
|
||||
func registerConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Release Channel",
|
||||
Key: releaseChannelKey,
|
||||
Description: "The Release Channel changes which updates are applied. When using beta, you will receive new features earlier and Portmaster will update more frequently. Some beta or experimental features are also available in the stable release channel.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelBeta,
|
||||
RequiresRestart: false,
|
||||
DefaultValue: releaseChannelStable,
|
||||
ExternalOptType: "string list",
|
||||
ValidationRegex: fmt.Sprintf("^(%s|%s)$", releaseChannelStable, releaseChannelBeta),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.RegisterEventHook("config", "config change", "update registry config", updateRegistryConfig)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
releaseChannel = config.GetAsString(releaseChannelKey, releaseChannelStable)
|
||||
devMode = config.GetAsBool("core/devMode", false)
|
||||
}
|
||||
|
||||
func updateRegistryConfig(_ context.Context, _ interface{}) error {
|
||||
changed := false
|
||||
if releaseChannel() != previousReleaseChannel {
|
||||
registry.SetBeta(releaseChannel() == releaseChannelBeta)
|
||||
previousReleaseChannel = releaseChannel()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if devMode() != previousDevMode {
|
||||
registry.SetBeta(devMode())
|
||||
previousDevMode = devMode()
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
registry.SelectVersions()
|
||||
module.TriggerEvent(eventVersionUpdate, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package updates
|
||||
|
||||
// current paths:
|
||||
// all/ui/assets.zip
|
||||
// all/ui/modules/base.zip
|
||||
// all/ui/modules/settings.zip
|
||||
// all/ui/modules/profilemgr.zip
|
||||
// all/ui/modules/monitor.zip
|
||||
// linux_amd64/app/portmaster-ui
|
129
updates/export.go
Normal file
129
updates/export.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
// database key for update information
|
||||
const (
|
||||
versionsDBKey = "core:status/versions"
|
||||
)
|
||||
|
||||
// working vars
|
||||
var (
|
||||
versionExport *versions
|
||||
versionExportDB = database.NewInterface(nil)
|
||||
versionExportHook *database.RegisteredHook
|
||||
)
|
||||
|
||||
// versions holds updates status information.
|
||||
type versions struct {
|
||||
record.Base
|
||||
lock sync.Mutex
|
||||
|
||||
Core *info.Info
|
||||
Resources map[string]*updater.Resource
|
||||
|
||||
internalSave bool
|
||||
}
|
||||
|
||||
func initVersionExport() (err error) {
|
||||
// init export struct
|
||||
versionExport = &versions{
|
||||
internalSave: true,
|
||||
}
|
||||
versionExport.SetKey(versionsDBKey)
|
||||
|
||||
// attach hook to database
|
||||
versionExportHook, err = database.RegisterHook(query.New(versionsDBKey), &exportHook{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module.RegisterEventHook(
|
||||
"updates",
|
||||
eventVersionUpdate,
|
||||
"export version status",
|
||||
export,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopVersionExport() error {
|
||||
return versionExportHook.Cancel()
|
||||
}
|
||||
|
||||
var exportMicroTaskName = "update version status"
|
||||
|
||||
func export(_ context.Context, _ interface{}) error {
|
||||
// populate
|
||||
versionExport.lock.Lock()
|
||||
versionExport.Core = info.GetInfo()
|
||||
versionExport.Resources = registry.Export()
|
||||
versionExport.lock.Unlock()
|
||||
|
||||
// save
|
||||
err := versionExportDB.Put(versionExport)
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to export versions: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock locks the versionExport and all associated resources.
|
||||
func (v *versions) Lock() {
|
||||
// lock self
|
||||
v.lock.Lock()
|
||||
|
||||
// lock all resources
|
||||
for _, res := range v.Resources {
|
||||
res.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
// Lock unlocks the versionExport and all associated resources.
|
||||
func (v *versions) Unlock() {
|
||||
// unlock all resources
|
||||
for _, res := range v.Resources {
|
||||
res.Unlock()
|
||||
}
|
||||
|
||||
// unlock self
|
||||
v.lock.Unlock()
|
||||
}
|
||||
|
||||
type exportHook struct {
|
||||
database.HookBase
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface.
|
||||
func (eh *exportHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var errInternalRecord = errors.New("may not modify internal record")
|
||||
|
||||
// PrePut implements the Hook interface.
|
||||
func (eh *exportHook) PrePut(r record.Record) (record.Record, error) {
|
||||
if r.IsWrapped() {
|
||||
return nil, errInternalRecord
|
||||
}
|
||||
ve, ok := r.(*versions)
|
||||
if !ok {
|
||||
return nil, errInternalRecord
|
||||
}
|
||||
if !ve.internalSave {
|
||||
return nil, errInternalRecord
|
||||
}
|
||||
return r, nil
|
||||
}
|
126
updates/fetch.go
126
updates/fetch.go
|
@ -1,126 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/google/renameio"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
updateURLs = []string{
|
||||
"https://updates.safing.io",
|
||||
}
|
||||
)
|
||||
|
||||
func fetchFile(realFilepath, updateFilepath string, tries int) error {
|
||||
// backoff when retrying
|
||||
if tries > 0 {
|
||||
time.Sleep(time.Duration(tries*tries) * time.Second)
|
||||
}
|
||||
|
||||
// create URL
|
||||
downloadURL, err := joinURLandPath(updateURLs[tries%len(updateURLs)], updateFilepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error build url (%s + %s): %s", updateURLs[tries%len(updateURLs)], updateFilepath, err)
|
||||
}
|
||||
|
||||
// check destination dir
|
||||
dirPath := filepath.Dir(realFilepath)
|
||||
err = updateStorage.EnsureAbsPath(dirPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create updates folder: %s", dirPath)
|
||||
}
|
||||
|
||||
// open file for writing
|
||||
atomicFile, err := renameio.TempFile(tmpStorage.Path, realFilepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create temp file for download: %s", err)
|
||||
}
|
||||
defer atomicFile.Cleanup()
|
||||
|
||||
// start file download
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching url (%s): %s", downloadURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// download and write file
|
||||
n, err := io.Copy(atomicFile, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed downloading %s: %s", downloadURL, err)
|
||||
}
|
||||
if resp.ContentLength != n {
|
||||
return fmt.Errorf("download unfinished, written %d out of %d bytes", n, resp.ContentLength)
|
||||
}
|
||||
|
||||
// finalize file
|
||||
err = atomicFile.CloseAtomicallyReplace()
|
||||
if err != nil {
|
||||
return fmt.Errorf("updates: failed to finalize file %s: %s", realFilepath, err)
|
||||
}
|
||||
// set permissions
|
||||
if runtime.GOOS != "windows" {
|
||||
// FIXME: only set executable files to 0755, set other to 0644
|
||||
err = os.Chmod(realFilepath, 0755)
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to set permissions on downloaded file %s: %s", realFilepath, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("updates: fetched %s (stored to %s)", downloadURL, realFilepath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchData(downloadPath string, tries int) ([]byte, error) {
|
||||
// backoff when retrying
|
||||
if tries > 0 {
|
||||
time.Sleep(time.Duration(tries*tries) * time.Second)
|
||||
}
|
||||
|
||||
// create URL
|
||||
downloadURL, err := joinURLandPath(updateURLs[tries%len(updateURLs)], downloadPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error build url (%s + %s): %s", updateURLs[tries%len(updateURLs)], downloadPath, err)
|
||||
}
|
||||
|
||||
// start file download
|
||||
resp, err := http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching url (%s): %s", downloadURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// download and write file
|
||||
buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
|
||||
n, err := io.Copy(buf, resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed downloading %s: %s", downloadURL, err)
|
||||
}
|
||||
if resp.ContentLength != n {
|
||||
return nil, fmt.Errorf("download unfinished, written %d out of %d bytes", n, resp.ContentLength)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func joinURLandPath(baseURL, urlPath string) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, urlPath)
|
||||
return u.String(), nil
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package updates
|
||||
|
||||
// File represents a file from the update system.
|
||||
type File struct {
|
||||
filepath string
|
||||
version string
|
||||
stable bool
|
||||
}
|
||||
|
||||
// NewFile combines update file attributes into an easy to use object.
|
||||
func NewFile(filepath string, version string, stable bool) *File {
|
||||
return &File{
|
||||
filepath: filepath,
|
||||
version: version,
|
||||
stable: stable,
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns the filepath of the file.
|
||||
func (f *File) Path() string {
|
||||
return f.filepath
|
||||
}
|
||||
|
||||
// Version returns the version of the file.
|
||||
func (f *File) Version() string {
|
||||
return f.version
|
||||
}
|
||||
|
||||
// Stable returns whether the file is from a stable release.
|
||||
func (f *File) Stable() bool {
|
||||
return f.stable
|
||||
}
|
||||
|
||||
// Open opens the file and returns the
|
||||
func (f *File) Open() {
|
||||
|
||||
}
|
||||
|
||||
// ReportError reports an error back to Safing. This will not automatically blacklist the file.
|
||||
func (f *File) ReportError() {
|
||||
|
||||
}
|
||||
|
||||
// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on.
|
||||
func (f *File) Blacklist() {
|
||||
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+b?`)
|
||||
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
|
||||
)
|
||||
|
||||
// GetIdentifierAndVersion splits the given file path into its identifier and version.
|
||||
func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
|
||||
// extract version
|
||||
rawVersion := fileVersionRegex.FindString(versionedPath)
|
||||
if rawVersion == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// replace - with . and trim _
|
||||
version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", -1)
|
||||
|
||||
// put together without version
|
||||
i := strings.Index(versionedPath, rawVersion)
|
||||
if i < 0 {
|
||||
// extracted version not in string (impossible)
|
||||
return "", "", false
|
||||
}
|
||||
return versionedPath[:i] + versionedPath[i+len(rawVersion):], version, true
|
||||
}
|
||||
|
||||
// GetVersionedPath combines the identifier and version and returns it as a file path.
|
||||
func GetVersionedPath(identifier, version string) (versionedPath string) {
|
||||
// split in half
|
||||
splittedFilePath := strings.SplitN(identifier, ".", 2)
|
||||
// replace . with -
|
||||
transformedVersion := strings.Replace(version, ".", "-", -1)
|
||||
|
||||
// put together
|
||||
if len(splittedFilePath) == 1 {
|
||||
return fmt.Sprintf("%s_v%s", splittedFilePath[0], transformedVersion)
|
||||
}
|
||||
return fmt.Sprintf("%s_v%s.%s", splittedFilePath[0], transformedVersion, splittedFilePath[1])
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testRegexMatch(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
|
||||
if testRegex.MatchString(testString) != shouldMatch {
|
||||
if shouldMatch {
|
||||
t.Errorf("regex %s should match %s", testRegex, testString)
|
||||
} else {
|
||||
t.Errorf("regex %s should not match %s", testRegex, testString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRegexFind(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
|
||||
if (testRegex.FindString(testString) != "") != shouldMatch {
|
||||
if shouldMatch {
|
||||
t.Errorf("regex %s should find %s", testRegex, testString)
|
||||
} else {
|
||||
t.Errorf("regex %s should not find %s", testRegex, testString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexes(t *testing.T) {
|
||||
testRegexMatch(t, rawVersionRegex, "0.1.2", true)
|
||||
testRegexMatch(t, rawVersionRegex, "0.1.2*", true)
|
||||
testRegexMatch(t, rawVersionRegex, "0.1.2b", true)
|
||||
testRegexMatch(t, rawVersionRegex, "0.1.2b*", true)
|
||||
testRegexMatch(t, rawVersionRegex, "12.13.14", true)
|
||||
|
||||
testRegexMatch(t, rawVersionRegex, "v0.1.2", false)
|
||||
testRegexMatch(t, rawVersionRegex, "0.", false)
|
||||
testRegexMatch(t, rawVersionRegex, "0.1", false)
|
||||
testRegexMatch(t, rawVersionRegex, "0.1.", false)
|
||||
testRegexMatch(t, rawVersionRegex, ".1.2", false)
|
||||
testRegexMatch(t, rawVersionRegex, ".1.", false)
|
||||
testRegexMatch(t, rawVersionRegex, "012345", false)
|
||||
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3", true)
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3.exe", true)
|
||||
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file_v1.2.3", false)
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file_1-2-3", false)
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2", false)
|
||||
testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
|
||||
}
|
|
@ -1,107 +1,38 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrNotFound = errors.New("the requested file could not be found")
|
||||
ErrNotAvailableLocally = errors.New("the requested file is not available locally")
|
||||
"github.com/safing/portbase/updater"
|
||||
)
|
||||
|
||||
// GetPlatformFile returns the latest platform specific file identified by the given identifier.
|
||||
func GetPlatformFile(identifier string) (*File, error) {
|
||||
func GetPlatformFile(identifier string) (*updater.File, error) {
|
||||
identifier = path.Join(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH), identifier)
|
||||
// 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 loadOrFetchFile(identifier, true)
|
||||
}
|
||||
|
||||
// GetLocalPlatformFile returns the latest platform specific file identified by the given identifier, that is available locally.
|
||||
func GetLocalPlatformFile(identifier string) (*File, error) {
|
||||
identifier = path.Join(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH), identifier)
|
||||
// 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 loadOrFetchFile(identifier, false)
|
||||
file, err := registry.GetFile(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.TriggerEvent(eventVersionUpdate, nil)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// GetFile returns the latest generic file identified by the given identifier.
|
||||
func GetFile(identifier string) (*File, error) {
|
||||
func GetFile(identifier string) (*updater.File, error) {
|
||||
identifier = path.Join("all", identifier)
|
||||
return loadOrFetchFile(identifier, true)
|
||||
}
|
||||
|
||||
// GetLocalFile returns the latest generic file identified by the given identifier, that is available locally.
|
||||
func GetLocalFile(identifier string) (*File, error) {
|
||||
identifier = path.Join("all", identifier)
|
||||
return loadOrFetchFile(identifier, false)
|
||||
}
|
||||
|
||||
func getLatestFilePath(identifier string) (versionedFilePath, version string, stable bool, ok bool) {
|
||||
updatesLock.RLock()
|
||||
defer updatesLock.RUnlock()
|
||||
|
||||
version, ok = stableUpdates[identifier]
|
||||
if !ok {
|
||||
version, ok = localUpdates[identifier]
|
||||
if !ok {
|
||||
log.Tracef("updates: file %s does not exist", identifier)
|
||||
return "", "", false, false
|
||||
// TODO: if in development mode, reload latest index to check for newly sideloaded updates
|
||||
// err := reloadLatest()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix for stable release
|
||||
return GetVersionedPath(identifier, version), version, false, true
|
||||
}
|
||||
|
||||
func loadOrFetchFile(identifier string, fetch bool) (*File, error) {
|
||||
versionedFilePath, version, stable, ok := getLatestFilePath(identifier)
|
||||
if !ok {
|
||||
// TODO: if in development mode, search updates dir for sideloaded apps
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// build final filepath
|
||||
realFilePath := filepath.Join(updateStorage.Path, filepath.FromSlash(versionedFilePath))
|
||||
if _, err := os.Stat(realFilePath); err == nil {
|
||||
// file exists
|
||||
updateUsedStatus(identifier, version)
|
||||
return NewFile(realFilePath, version, stable), nil
|
||||
}
|
||||
|
||||
// check download dir
|
||||
err := tmpStorage.Ensure()
|
||||
file, err := registry.GetFile(identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not prepare tmp directory for download: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !fetch {
|
||||
return nil, ErrNotAvailableLocally
|
||||
}
|
||||
|
||||
// download file
|
||||
log.Tracef("updates: starting download of %s", versionedFilePath)
|
||||
for tries := 0; tries < 5; tries++ {
|
||||
err = fetchFile(realFilePath, versionedFilePath, tries)
|
||||
if err != nil {
|
||||
log.Tracef("updates: failed to download %s: %s, retrying (%d)", versionedFilePath, err, tries+1)
|
||||
} else {
|
||||
updateUsedStatus(identifier, version)
|
||||
return NewFile(realFilePath, version, stable), nil
|
||||
}
|
||||
}
|
||||
log.Warningf("updates: failed to download %s: %s", versionedFilePath, err)
|
||||
return nil, err
|
||||
module.TriggerEvent(eventVersionUpdate, nil)
|
||||
return file, nil
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
package updates
|
||||
|
||||
import "testing"
|
||||
|
||||
func testBuildVersionedFilePath(t *testing.T, identifier, version, expectedVersionedFilePath string) {
|
||||
updatesLock.Lock()
|
||||
stableUpdates[identifier] = version
|
||||
// betaUpdates[identifier] = version
|
||||
updatesLock.Unlock()
|
||||
|
||||
versionedFilePath, _, _, ok := getLatestFilePath(identifier)
|
||||
if !ok {
|
||||
t.Errorf("identifier %s should exist", identifier)
|
||||
}
|
||||
if versionedFilePath != expectedVersionedFilePath {
|
||||
t.Errorf("unexpected versionedFilePath: %s", versionedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildVersionedFilePath(t *testing.T) {
|
||||
testBuildVersionedFilePath(t, "path/to/asset.zip", "1.2.3", "path/to/asset_v1-2-3.zip")
|
||||
testBuildVersionedFilePath(t, "path/to/asset.tar.gz", "1.2.3b", "path/to/asset_v1-2-3b.tar.gz")
|
||||
testBuildVersionedFilePath(t, "path/to/asset", "1.2.3b", "path/to/asset_v1-2-3b")
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils"
|
||||
|
||||
semver "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
stableUpdates = make(map[string]string)
|
||||
betaUpdates = make(map[string]string)
|
||||
localUpdates = make(map[string]string)
|
||||
updatesLock sync.RWMutex
|
||||
)
|
||||
|
||||
// LoadLatest (re)loads the latest available updates from disk.
|
||||
func LoadLatest() error {
|
||||
newLocalUpdates := make(map[string]string)
|
||||
|
||||
// all
|
||||
prefix := "all"
|
||||
new, err1 := ScanForLatest(filepath.Join(updateStorage.Path, prefix), false)
|
||||
for key, val := range new {
|
||||
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
||||
}
|
||||
|
||||
// os_platform
|
||||
prefix = fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||
new, err2 := ScanForLatest(filepath.Join(updateStorage.Path, prefix), false)
|
||||
for key, val := range new {
|
||||
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
||||
}
|
||||
|
||||
if err1 != nil && err2 != nil {
|
||||
return fmt.Errorf("could not load latest update versions: %s, %s", err1, err2)
|
||||
}
|
||||
|
||||
log.Tracef("updates: loading latest updates:")
|
||||
|
||||
for key, val := range newLocalUpdates {
|
||||
log.Tracef("updates: %s v%s", key, val)
|
||||
}
|
||||
|
||||
updatesLock.Lock()
|
||||
localUpdates = newLocalUpdates
|
||||
updatesLock.Unlock()
|
||||
|
||||
log.Tracef("updates: load complete")
|
||||
|
||||
// update version status
|
||||
updatesLock.RLock()
|
||||
defer updatesLock.RUnlock()
|
||||
updateStatus(versionClassLocal, localUpdates)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScanForLatest scan the local update directory and returns a map of the latest/newest component versions.
|
||||
func ScanForLatest(baseDir string, hardFail bool) (latest map[string]string, lastError error) {
|
||||
var added int
|
||||
latest = make(map[string]string)
|
||||
|
||||
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
lastError = fmt.Errorf("updates: could not read %s: %s", path, err)
|
||||
if hardFail {
|
||||
return lastError
|
||||
}
|
||||
log.Warning(lastError.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
added++
|
||||
}
|
||||
|
||||
relativePath, err := filepath.Rel(baseDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relativePath = filepath.ToSlash(relativePath)
|
||||
identifierPath, version, ok := GetIdentifierAndVersion(relativePath)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// add/update index
|
||||
storedVersion, ok := latest[identifierPath]
|
||||
if ok {
|
||||
parsedVersion, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
lastError = fmt.Errorf("updates: could not parse version of %s: %s", path, err)
|
||||
if hardFail {
|
||||
return lastError
|
||||
}
|
||||
log.Warning(lastError.Error())
|
||||
}
|
||||
parsedStoredVersion, err := semver.NewVersion(storedVersion)
|
||||
if err != nil {
|
||||
lastError = fmt.Errorf("updates: could not parse version of %s: %s", path, err)
|
||||
if hardFail {
|
||||
return lastError
|
||||
}
|
||||
log.Warning(lastError.Error())
|
||||
}
|
||||
// compare
|
||||
if parsedVersion.GreaterThan(parsedStoredVersion) {
|
||||
latest[identifierPath] = version
|
||||
}
|
||||
} else {
|
||||
latest[identifierPath] = version
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if lastError != nil {
|
||||
if hardFail {
|
||||
return nil, lastError
|
||||
}
|
||||
if added == 0 {
|
||||
return latest, lastError
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
// LoadIndexes loads the current update indexes from disk.
|
||||
func LoadIndexes() error {
|
||||
data, err := ioutil.ReadFile(filepath.Join(updateStorage.Path, "stable.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newStableUpdates := make(map[string]string)
|
||||
err = json.Unmarshal(data, &newStableUpdates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(newStableUpdates) == 0 {
|
||||
return errors.New("stable.json is empty")
|
||||
}
|
||||
|
||||
log.Tracef("updates: loaded stable.json")
|
||||
|
||||
updatesLock.Lock()
|
||||
stableUpdates = newStableUpdates
|
||||
updatesLock.Unlock()
|
||||
|
||||
// update version status
|
||||
updatesLock.RLock()
|
||||
defer updatesLock.RUnlock()
|
||||
updateStatus(versionClassStable, stableUpdates)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSymlinks creates a directory structure with unversions symlinks to the given updates list.
|
||||
func CreateSymlinks(symlinkRoot, updateStorage *utils.DirStructure, updatesList map[string]string) error {
|
||||
err := os.RemoveAll(symlinkRoot.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wipe symlink root: %s", err)
|
||||
}
|
||||
|
||||
err = symlinkRoot.Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create symlink root: %s", err)
|
||||
}
|
||||
|
||||
for identifier, version := range updatesList {
|
||||
targetPath := filepath.Join(updateStorage.Path, filepath.FromSlash(GetVersionedPath(identifier, version)))
|
||||
linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(identifier))
|
||||
linkPathDir := filepath.Dir(linkPath)
|
||||
|
||||
err = symlinkRoot.EnsureAbsPath(linkPathDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create dir for link: %s", err)
|
||||
}
|
||||
|
||||
relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative target path: %s", err)
|
||||
}
|
||||
|
||||
err = os.Symlink(relativeTargetPath, linkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to link %s: %s", identifier, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testLoadLatestScope(t *testing.T, basePath, filePath, expectedIdentifier, expectedVersion string) {
|
||||
fullPath := filepath.Join(basePath, filePath)
|
||||
|
||||
// create dir
|
||||
dirPath := filepath.Dir(fullPath)
|
||||
err := os.MkdirAll(dirPath, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create test dir: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// touch file
|
||||
err = ioutil.WriteFile(fullPath, []byte{}, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create test file: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// run loadLatestScope
|
||||
latest, err := ScanForLatest(basePath, true)
|
||||
if err != nil {
|
||||
t.Errorf("could not update latest: %s\n", err)
|
||||
return
|
||||
}
|
||||
for key, val := range latest {
|
||||
localUpdates[key] = val
|
||||
}
|
||||
|
||||
// test result
|
||||
version, ok := localUpdates[expectedIdentifier]
|
||||
if !ok {
|
||||
t.Errorf("identifier %s not in map", expectedIdentifier)
|
||||
t.Errorf("current map: %v", localUpdates)
|
||||
}
|
||||
if version != expectedVersion {
|
||||
t.Errorf("unexpected version for %s: %s", filePath, version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadLatestScope(t *testing.T) {
|
||||
|
||||
updatesLock.Lock()
|
||||
defer updatesLock.Unlock()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "testing_")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create test dir: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "1.2.3")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4b.zip", "all/ui/assets.zip", "1.2.4b")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-5.zip", "all/ui/assets.zip", "1.2.5")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "1.3.4")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v2-3-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "2.3.4")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "1.2.3")
|
||||
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v2-1-1", "os_platform/portmaster/portmaster", "2.1.1")
|
||||
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "2.1.1")
|
||||
|
||||
}
|
141
updates/main.go
141
updates/main.go
|
@ -1,87 +1,118 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/core/structure"
|
||||
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portbase/utils"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portmaster/core/structure"
|
||||
)
|
||||
|
||||
const (
|
||||
isWindows = runtime.GOOS == "windows"
|
||||
onWindows = runtime.GOOS == "windows"
|
||||
|
||||
releaseChannelKey = "core/releaseChannel"
|
||||
releaseChannelStable = "stable"
|
||||
releaseChannelBeta = "beta"
|
||||
|
||||
eventVersionUpdate = "active version update"
|
||||
eventResourceUpdate = "resource update"
|
||||
)
|
||||
|
||||
var (
|
||||
updateStorage *utils.DirStructure
|
||||
tmpStorage *utils.DirStructure
|
||||
module *modules.Module
|
||||
registry *updater.ResourceRegistry
|
||||
)
|
||||
|
||||
// SetDataRoot sets the data root from which the updates module derives its paths.
|
||||
func SetDataRoot(root *utils.DirStructure) {
|
||||
if root != nil && updateStorage == nil {
|
||||
updateStorage = root.ChildDir("updates", 0755)
|
||||
tmpStorage = updateStorage.ChildDir("tmp", 0777)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
modules.Register("updates", prep, start, stop, "core")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
SetDataRoot(structure.Root())
|
||||
if updateStorage == nil {
|
||||
return errors.New("update storage path is not set")
|
||||
}
|
||||
|
||||
err := updateStorage.Ensure()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status.Core = info.GetInfo()
|
||||
|
||||
return nil
|
||||
module = modules.Register("updates", registerConfig, start, stop, "core")
|
||||
module.RegisterEvent(eventVersionUpdate)
|
||||
module.RegisterEvent(eventResourceUpdate)
|
||||
}
|
||||
|
||||
func start() error {
|
||||
err := initUpdateStatusHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initConfig()
|
||||
|
||||
err = LoadIndexes()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// download indexes
|
||||
log.Infof("updates: downloading update index...")
|
||||
|
||||
err = UpdateIndexes()
|
||||
if err != nil {
|
||||
log.Errorf("updates: failed to download update index: %s", err)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
var mandatoryUpdates []string
|
||||
if onWindows {
|
||||
mandatoryUpdates = []string{
|
||||
platform("core/portmaster-core.exe"),
|
||||
platform("control/portmaster-control.exe"),
|
||||
platform("app/portmaster-app.exe"),
|
||||
platform("notifier/portmaster-notifier.exe"),
|
||||
platform("notifier/portmaster-snoretoast.exe"),
|
||||
}
|
||||
} else {
|
||||
mandatoryUpdates = []string{
|
||||
platform("core/portmaster-core"),
|
||||
platform("control/portmaster-control"),
|
||||
platform("app/portmaster-app"),
|
||||
platform("notifier/portmaster-notifier"),
|
||||
}
|
||||
}
|
||||
|
||||
err = LoadLatest()
|
||||
// create registry
|
||||
registry = &updater.ResourceRegistry{
|
||||
Name: "updates",
|
||||
UpdateURLs: []string{
|
||||
"https://updates.safing.io",
|
||||
},
|
||||
MandatoryUpdates: mandatoryUpdates,
|
||||
Beta: releaseChannel() == releaseChannelBeta,
|
||||
DevMode: devMode(),
|
||||
Online: true,
|
||||
}
|
||||
// initialize
|
||||
err := registry.Initialize(structure.Root().ChildDir("updates", 0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go updater()
|
||||
go updateNotifier()
|
||||
err = registry.LoadIndexes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = registry.ScanStorage("")
|
||||
if err != nil {
|
||||
log.Warningf("updates: error during storage scan: %s", err)
|
||||
}
|
||||
|
||||
registry.SelectVersions()
|
||||
module.TriggerEvent(eventVersionUpdate, nil)
|
||||
|
||||
err = initVersionExport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start updater task
|
||||
module.NewTask("updater", func(ctx context.Context, task *modules.Task) {
|
||||
err := registry.DownloadUpdates(ctx)
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to update: %s", err)
|
||||
}
|
||||
module.TriggerEvent(eventResourceUpdate, nil)
|
||||
}).Repeat(24 * time.Hour).MaxDelay(1 * time.Hour).Schedule(time.Now().Add(10 * time.Second))
|
||||
|
||||
// react to upgrades
|
||||
initUpgrader()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
// delete download tmp dir
|
||||
return os.RemoveAll(tmpStorage.Path)
|
||||
if registry != nil {
|
||||
return registry.Cleanup()
|
||||
}
|
||||
|
||||
return stopVersionExport()
|
||||
}
|
||||
|
||||
func platform(identifier string) string {
|
||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/notifications"
|
||||
)
|
||||
|
||||
const coreIdentifier = "core/portmaster-core"
|
||||
|
||||
var lastNotified time.Time
|
||||
|
||||
func updateNotifier() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
for {
|
||||
ident := coreIdentifier
|
||||
if isWindows {
|
||||
ident += ".exe"
|
||||
}
|
||||
|
||||
file, err := GetLocalPlatformFile(ident)
|
||||
if err == nil {
|
||||
status.Lock()
|
||||
liveVersion := status.Core.Version
|
||||
status.Unlock()
|
||||
|
||||
if file.Version() != liveVersion {
|
||||
|
||||
// create notification
|
||||
(¬ifications.Notification{
|
||||
ID: "updates-core-update-available",
|
||||
Message: fmt.Sprintf("There is an update available for the Portmaster core (v%s), please restart the Portmaster to apply the update.", file.Version()),
|
||||
Type: notifications.Info,
|
||||
Expires: time.Now().Add(1 * time.Minute).Unix(),
|
||||
}).Save()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
// database key for update information
|
||||
const (
|
||||
statusDBKey = "core:status/updates"
|
||||
)
|
||||
|
||||
// version type
|
||||
type versionClass int
|
||||
|
||||
const (
|
||||
versionClassLocal versionClass = iota
|
||||
versionClassStable
|
||||
versionClassBeta
|
||||
)
|
||||
|
||||
// working vars
|
||||
var (
|
||||
status *versionStatus
|
||||
|
||||
statusDB = database.NewInterface(nil)
|
||||
statusHook *database.RegisteredHook
|
||||
enableStatusSave = abool.NewBool(false)
|
||||
)
|
||||
|
||||
func init() {
|
||||
status = &versionStatus{
|
||||
Modules: make(map[string]*versionStatusEntry),
|
||||
}
|
||||
status.SetKey(statusDBKey)
|
||||
}
|
||||
|
||||
// versionStatus holds update version status information.
|
||||
type versionStatus struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
Core *info.Info
|
||||
Modules map[string]*versionStatusEntry
|
||||
}
|
||||
|
||||
func (vs *versionStatus) save() {
|
||||
enableStatusSave.SetTo(true)
|
||||
err := statusDB.Put(vs)
|
||||
if err != nil {
|
||||
log.Warningf("could not save updates version status: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// versionStatusEntry holds information about the update status of a module.
|
||||
type versionStatusEntry struct {
|
||||
LastVersionUsed string
|
||||
LocalVersion string
|
||||
StableVersion string
|
||||
BetaVersion string
|
||||
}
|
||||
|
||||
func updateUsedStatus(identifier string, version string) {
|
||||
status.Lock()
|
||||
defer status.Unlock()
|
||||
|
||||
entry, ok := status.Modules[identifier]
|
||||
if !ok {
|
||||
entry = &versionStatusEntry{}
|
||||
status.Modules[identifier] = entry
|
||||
}
|
||||
|
||||
entry.LastVersionUsed = version
|
||||
|
||||
log.Tracef("updates: updated last used version of %s: %s", identifier, version)
|
||||
|
||||
go status.save()
|
||||
}
|
||||
|
||||
func updateStatus(vClass versionClass, state map[string]string) {
|
||||
status.Lock()
|
||||
defer status.Unlock()
|
||||
|
||||
for identifier, version := range state {
|
||||
|
||||
entry, ok := status.Modules[identifier]
|
||||
if !ok {
|
||||
entry = &versionStatusEntry{}
|
||||
status.Modules[identifier] = entry
|
||||
}
|
||||
|
||||
switch vClass {
|
||||
case versionClassLocal:
|
||||
entry.LocalVersion = version
|
||||
case versionClassStable:
|
||||
entry.StableVersion = version
|
||||
case versionClassBeta:
|
||||
entry.BetaVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
go status.save()
|
||||
}
|
||||
|
||||
type updateStatusHook struct {
|
||||
database.HookBase
|
||||
}
|
||||
|
||||
// UsesPrePut implements the Hook interface.
|
||||
func (sh *updateStatusHook) UsesPrePut() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PrePut implements the Hook interface.
|
||||
func (sh *updateStatusHook) PrePut(r record.Record) (record.Record, error) {
|
||||
if enableStatusSave.SetToIf(true, false) {
|
||||
return r, nil
|
||||
}
|
||||
return nil, errors.New("may only be changed by updates module")
|
||||
}
|
||||
|
||||
func initUpdateStatusHook() (err error) {
|
||||
statusHook, err = database.RegisterHook(query.New(statusDBKey), &updateStatusHook{})
|
||||
return err
|
||||
}
|
53
updates/testing.go
Normal file
53
updates/testing.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/updater"
|
||||
"github.com/safing/portbase/utils"
|
||||
)
|
||||
|
||||
// InitForTesting initializes the update module directly. This is intended to be only used by unit tests that require the updates module.
|
||||
func InitForTesting() error {
|
||||
// create registry
|
||||
registry = &updater.ResourceRegistry{
|
||||
Name: "testing-updates",
|
||||
UpdateURLs: []string{
|
||||
"https://updates.safing.io",
|
||||
},
|
||||
Beta: false,
|
||||
DevMode: false,
|
||||
Online: true,
|
||||
}
|
||||
|
||||
// set data dir
|
||||
root := utils.NewDirStructure(
|
||||
filepath.Join(os.TempDir(), "pm-testing"),
|
||||
0755,
|
||||
)
|
||||
err := root.Ensure()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// initialize
|
||||
err = registry.Initialize(root.ChildDir("updates", 0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = registry.LoadIndexes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = registry.ScanStorage("")
|
||||
if err != nil {
|
||||
log.Warningf("updates: error during storage scan: %s", err)
|
||||
}
|
||||
|
||||
registry.SelectVersions()
|
||||
return nil
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
func updater() {
|
||||
time.Sleep(10 * time.Second)
|
||||
for {
|
||||
err := UpdateIndexes()
|
||||
if err != nil {
|
||||
log.Warningf("updates: updating index failed: %s", err)
|
||||
}
|
||||
err = DownloadUpdates()
|
||||
if err != nil {
|
||||
log.Warningf("updates: downloading updates failed: %s", err)
|
||||
}
|
||||
err = runFileUpgrades()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to upgrade portmaster-control: %s", err)
|
||||
}
|
||||
err = cleanOldUpgradedFiles()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to clean old upgraded files: %s", err)
|
||||
}
|
||||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
func markFileForDownload(identifier string) {
|
||||
// get file
|
||||
_, ok := localUpdates[identifier]
|
||||
// only mark if it does not yet exist
|
||||
if !ok {
|
||||
localUpdates[identifier] = "loading..."
|
||||
}
|
||||
}
|
||||
|
||||
func markPlatformFileForDownload(identifier string) {
|
||||
// add platform prefix
|
||||
identifier = path.Join(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH), identifier)
|
||||
// mark file
|
||||
markFileForDownload(identifier)
|
||||
}
|
||||
|
||||
// UpdateIndexes downloads the current update indexes.
|
||||
func UpdateIndexes() (err error) {
|
||||
// download new indexes
|
||||
var data []byte
|
||||
for tries := 0; tries < 3; tries++ {
|
||||
data, err = fetchData("stable.json", tries)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download: %s", err)
|
||||
}
|
||||
|
||||
newStableUpdates := make(map[string]string)
|
||||
err = json.Unmarshal(data, &newStableUpdates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse: %s", err)
|
||||
}
|
||||
|
||||
if len(newStableUpdates) == 0 {
|
||||
return errors.New("index is empty")
|
||||
}
|
||||
|
||||
// update stable index
|
||||
updatesLock.Lock()
|
||||
stableUpdates = newStableUpdates
|
||||
updatesLock.Unlock()
|
||||
|
||||
// check dir
|
||||
err = updateStorage.Ensure()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save stable index
|
||||
err = ioutil.WriteFile(filepath.Join(updateStorage.Path, "stable.json"), data, 0644)
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to save new version of stable.json: %s", err)
|
||||
}
|
||||
|
||||
// update version status
|
||||
updatesLock.RLock()
|
||||
updateStatus(versionClassStable, stableUpdates)
|
||||
updatesLock.RUnlock()
|
||||
|
||||
// FIXME IN STABLE: correct log line
|
||||
log.Infof("updates: updated index stable.json (alpha/beta until we actually reach stable)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadUpdates checks if updates are available and downloads updates of used components.
|
||||
func DownloadUpdates() (err error) {
|
||||
|
||||
// ensure important components are always updated
|
||||
updatesLock.Lock()
|
||||
if runtime.GOOS == "windows" {
|
||||
markPlatformFileForDownload("core/portmaster-core.exe")
|
||||
markPlatformFileForDownload("control/portmaster-control.exe")
|
||||
markPlatformFileForDownload("app/portmaster-app.exe")
|
||||
markPlatformFileForDownload("notifier/portmaster-notifier.exe")
|
||||
markPlatformFileForDownload("notifier/portmaster-snoretoast.exe")
|
||||
} else {
|
||||
markPlatformFileForDownload("core/portmaster-core")
|
||||
markPlatformFileForDownload("control/portmaster-control")
|
||||
markPlatformFileForDownload("app/portmaster-app")
|
||||
markPlatformFileForDownload("notifier/portmaster-notifier")
|
||||
}
|
||||
updatesLock.Unlock()
|
||||
|
||||
// check download dir
|
||||
err = tmpStorage.Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prepare tmp directory for download: %s", err)
|
||||
}
|
||||
|
||||
// RLock for the remaining function
|
||||
updatesLock.RLock()
|
||||
defer updatesLock.RUnlock()
|
||||
|
||||
// update existing files
|
||||
log.Tracef("updates: updating existing files")
|
||||
for identifier, newVersion := range stableUpdates {
|
||||
oldVersion, ok := localUpdates[identifier]
|
||||
if ok && newVersion != oldVersion {
|
||||
|
||||
log.Tracef("updates: updating %s to %s", identifier, newVersion)
|
||||
filePath := GetVersionedPath(identifier, newVersion)
|
||||
realFilePath := filepath.Join(updateStorage.Path, filePath)
|
||||
for tries := 0; tries < 3; tries++ {
|
||||
err = fetchFile(realFilePath, filePath, tries)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to update %s to %s: %s", identifier, newVersion, err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
log.Tracef("updates: finished updating existing files")
|
||||
|
||||
// remove tmp folder after we are finished
|
||||
err = os.RemoveAll(tmpStorage.Path)
|
||||
if err != nil {
|
||||
log.Tracef("updates: failed to remove tmp dir %s after downloading updates: %s", updateStorage.Path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
package updates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/google/renameio"
|
||||
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portbase/updater"
|
||||
|
||||
processInfo "github.com/shirou/gopsutil/process"
|
||||
)
|
||||
|
@ -21,21 +27,102 @@ const (
|
|||
upgradedSuffix = "-upgraded"
|
||||
)
|
||||
|
||||
func runFileUpgrades() error {
|
||||
var (
|
||||
upgraderActive = abool.NewBool(false)
|
||||
dontUpgradeBefore = time.Now().Add(5 * time.Minute)
|
||||
pmCtrlUpdate *updater.File
|
||||
pmCoreUpdate *updater.File
|
||||
|
||||
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
|
||||
)
|
||||
|
||||
func initUpgrader() {
|
||||
module.RegisterEventHook(
|
||||
"updates",
|
||||
eventResourceUpdate,
|
||||
"run upgrades",
|
||||
upgrader,
|
||||
)
|
||||
}
|
||||
|
||||
func upgrader(_ context.Context, _ interface{}) error {
|
||||
// like a lock, but discard additional runs
|
||||
if !upgraderActive.SetToIf(false, true) {
|
||||
return nil
|
||||
}
|
||||
defer upgraderActive.SetTo(false)
|
||||
|
||||
// upgrade portmaster control
|
||||
err := upgradePortmasterControl()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to upgrade portmaster-control: %s", err)
|
||||
}
|
||||
|
||||
err = upgradeCoreNotify()
|
||||
if err != nil {
|
||||
log.Warningf("updates: failed to notify about core upgrade: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeCoreNotify() error {
|
||||
identifier := "core/portmaster-core" // identifier, use forward slash!
|
||||
if onWindows {
|
||||
identifier += ".exe"
|
||||
}
|
||||
|
||||
// check if we can upgrade
|
||||
if pmCoreUpdate == nil || pmCoreUpdate.UpgradeAvailable() {
|
||||
// get newest portmaster-control
|
||||
new, err := GetPlatformFile(identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pmCoreUpdate = new
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.GetInfo().Version != pmCoreUpdate.Version() {
|
||||
// create notification
|
||||
(¬ifications.Notification{
|
||||
ID: "updates-core-update-available",
|
||||
Message: fmt.Sprintf("There is an update available for the Portmaster core (v%s), please restart the Portmaster to apply the update.", pmCoreUpdate.Version()),
|
||||
Type: notifications.Info,
|
||||
}).Save()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradePortmasterControl() error {
|
||||
filename := "portmaster-control"
|
||||
if runtime.GOOS == "windows" {
|
||||
if onWindows {
|
||||
filename += ".exe"
|
||||
}
|
||||
|
||||
// get newest portmaster-control
|
||||
newFile, err := GetPlatformFile("control/" + filename) // identifier, use forward slash!
|
||||
// check if we can upgrade
|
||||
if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() {
|
||||
// get newest portmaster-control
|
||||
new, err := GetPlatformFile("control/" + filename) // identifier, use forward slash!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pmCtrlUpdate = new
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if registry tmp dir is ok
|
||||
err := registry.TmpDir().Ensure()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to prep updates tmp dir: %s", err)
|
||||
}
|
||||
|
||||
// update portmaster-control in data root
|
||||
rootControlPath := filepath.Join(filepath.Dir(updateStorage.Path), filename)
|
||||
err = upgradeFile(rootControlPath, newFile)
|
||||
rootControlPath := filepath.Join(filepath.Dir(registry.StorageDir().Path), filename)
|
||||
err = upgradeFile(rootControlPath, pmCtrlUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -50,7 +137,7 @@ func runFileUpgrades() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not get parent process name for upgrade checks: %s", err)
|
||||
}
|
||||
if !strings.HasPrefix(parentName, filename) {
|
||||
if parentName != filename {
|
||||
log.Tracef("updates: parent process does not seem to be portmaster-control, name is %s", parentName)
|
||||
return nil
|
||||
}
|
||||
|
@ -58,7 +145,7 @@ func runFileUpgrades() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not get parent process path for upgrade: %s", err)
|
||||
}
|
||||
err = upgradeFile(parentPath, newFile)
|
||||
err = upgradeFile(parentPath, pmCtrlUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -67,7 +154,7 @@ func runFileUpgrades() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func upgradeFile(fileToUpgrade string, file *File) error {
|
||||
func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
||||
fileExists := false
|
||||
_, err := os.Stat(fileToUpgrade)
|
||||
if err == nil {
|
||||
|
@ -75,12 +162,6 @@ func upgradeFile(fileToUpgrade string, file *File) error {
|
|||
fileExists = true
|
||||
}
|
||||
|
||||
// ensure that the tmp dir exists
|
||||
err = tmpStorage.Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create directory for upgrade process: %s", err)
|
||||
}
|
||||
|
||||
if fileExists {
|
||||
// get current version
|
||||
var currentVersion string
|
||||
|
@ -107,12 +188,18 @@ func upgradeFile(fileToUpgrade string, file *File) error {
|
|||
// 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("unable to check updates tmp dir for moving file that needs upgrade: %s", err)
|
||||
}
|
||||
|
||||
// maybe we're on windows and it's in use, try moving
|
||||
err = os.Rename(fileToUpgrade, filepath.Join(
|
||||
tmpStorage.Path,
|
||||
registry.TmpDir().Path,
|
||||
fmt.Sprintf(
|
||||
"%s-%d%s",
|
||||
GetVersionedPath(filepath.Base(fileToUpgrade), currentVersion),
|
||||
updater.GetVersionedPath(filepath.Base(fileToUpgrade), currentVersion),
|
||||
time.Now().UTC().Unix(),
|
||||
upgradedSuffix,
|
||||
),
|
||||
|
@ -124,11 +211,10 @@ func upgradeFile(fileToUpgrade string, file *File) error {
|
|||
}
|
||||
|
||||
// copy upgrade
|
||||
// TODO: handle copy failure
|
||||
err = copyFile(file.Path(), fileToUpgrade)
|
||||
if err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
// try again
|
||||
time.Sleep(1 * time.Second)
|
||||
err = copyFile(file.Path(), fileToUpgrade)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -136,7 +222,7 @@ func upgradeFile(fileToUpgrade string, file *File) error {
|
|||
}
|
||||
|
||||
// check permissions
|
||||
if runtime.GOOS != "windows" {
|
||||
if !onWindows {
|
||||
info, err := os.Stat(fileToUpgrade)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info on %s: %s", fileToUpgrade, err)
|
||||
|
@ -153,11 +239,11 @@ func upgradeFile(fileToUpgrade string, file *File) error {
|
|||
|
||||
func copyFile(srcPath, dstPath string) (err error) {
|
||||
// open file for writing
|
||||
atomicDstFile, err := renameio.TempFile(tmpStorage.Path, dstPath)
|
||||
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create temp file for atomic copy: %s", err)
|
||||
}
|
||||
defer atomicDstFile.Cleanup()
|
||||
defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
|
||||
|
||||
// open source
|
||||
srcFile, err := os.Open(srcPath)
|
||||
|
@ -180,7 +266,3 @@ func copyFile(srcPath, dstPath string) (err error) {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanOldUpgradedFiles() error {
|
||||
return os.RemoveAll(tmpStorage.Path)
|
||||
}
|
||||
|
|
1
updates/uptool/.gitignore
vendored
1
updates/uptool/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
uptool
|
|
@ -1,41 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/safing/portbase/utils"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
updatesStorage *utils.DirStructure
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "uptool",
|
||||
Short: "helper tool for the update process",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Usage()
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
dir := args[0]
|
||||
|
||||
absPath, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatesStorage = utils.NewDirStructure(absPath, 0755)
|
||||
return nil
|
||||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portmaster/updates"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(scanCmd)
|
||||
}
|
||||
|
||||
var scanCmd = &cobra.Command{
|
||||
Use: "scan",
|
||||
Short: "Scan the specified directory and print the result",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: scan,
|
||||
}
|
||||
|
||||
func scan(cmd *cobra.Command, args []string) error {
|
||||
latest, err := updates.ScanForLatest(updatesStorage.Path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(latest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
return nil
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/safing/portmaster/updates"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update scans the specified directory and updates the index and symlink structure",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: update,
|
||||
}
|
||||
|
||||
func update(cmd *cobra.Command, args []string) error {
|
||||
latest, err := updates.ScanForLatest(updatesStorage.Path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(latest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(updatesStorage.Path, "stable.json"), data, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updates.CreateSymlinks(updatesStorage.ChildDir("latest", 0755), updatesStorage, latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Add table
Reference in a new issue