[WIP] updater rafactoring, minor improvments

This commit is contained in:
Vladimir Stoilov
2024-09-17 12:36:44 +03:00
parent 83ec18f552
commit 84a15b5fb3
18 changed files with 380 additions and 310 deletions

View File

@@ -35,20 +35,12 @@ type windowsService struct {
func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
startupComplete := make(chan struct{})
go func() {
for !ws.instance.Ready() {
time.Sleep(1 * time.Second)
}
startupComplete <- struct{}{}
}()
ws.instance.Start()
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
service:
for {
select {
case <-startupComplete:
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
case <-ws.instance.Stopped():
changes <- svc.Status{State: svc.StopPending}
break service
@@ -59,13 +51,13 @@ service:
case svc.Stop, svc.Shutdown:
ws.instance.Shutdown()
default:
log.Errorf("unexpected control request: #%d\n", c)
log.Errorf("unexpected control request: #%d", c)
}
}
}
// wait until everything else is finished
finishWg.Wait()
// finishWg.Wait()
log.Shutdown()
@@ -88,12 +80,12 @@ func run(instance *service.Instance) error {
// select service run type
svcRun := svc.Run
if !isService {
log.Warningf("running interactively, switching to debug execution (no real service).\n")
log.Warningf("running interactively, switching to debug execution (no real service).")
svcRun = debug.Run
go registerSignalHandler(instance)
}
runWg.Add(2)
runWg.Add(1)
// run service client
go func() {
@@ -105,29 +97,27 @@ func run(instance *service.Instance) error {
} else {
log.Infof("shuting down service")
}
instance.Shutdown()
runWg.Done()
}()
finishWg.Add(1)
// finishWg.Add(1)
// run service
go func() {
// run slightly delayed
time.Sleep(250 * time.Millisecond)
instance.Start()
// go func() {
// // run slightly delayed
// time.Sleep(250 * time.Millisecond)
if err != nil {
fmt.Printf("instance start failed: %s\n", err)
// if err != nil {
// fmt.Printf("instance start failed: %s\n", err)
// Print stack on start failure, if enabled.
if printStackOnExit {
printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE")
}
// // Print stack on start failure, if enabled.
// if printStackOnExit {
// printStackTo(os.Stdout, "PRINTING STACK ON START FAILURE")
// }
}
runWg.Done()
finishWg.Done()
}()
// }
// runWg.Done()
// finishWg.Done()
// }()
runWg.Wait()

View File

@@ -33,7 +33,9 @@ Update WIX installer template:
2. Replace the contents of `templates/main_original.wxs` with the repository version.
3. Replace the contents of `templates/main.wsx` and add the fallowing lines at the end of the file, inside the `Product` tag.
```xml
<!-- Service fragments -->
<CustomActionRef Id='InstallPortmasterService' />
<CustomActionRef Id='StopPortmasterService' />
<CustomActionRef Id='DeletePortmasterService' />
<!-- End Service fragments -->
```

View File

@@ -109,8 +109,10 @@
},
"wix": {
"fragmentPaths": [
"templates/service.wxs"
"templates/service.wxs",
"templates/files.wxs"
],
"componentGroupRefs": ["BinaryAndIntelFiles"],
"template": "templates/main.wxs"
}
},

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<DirectoryRef Id="INSTALLDIR">
<Directory Id="BinaryDir" Name="binary" />
<Directory Id="IntelDir" Name="intel" />
</DirectoryRef>
</Fragment>
<Fragment>
<Component Id="BinaryFiles" Directory="BinaryDir" Guid="850cdd31-424d-45f5-b8f0-95df950ebd0d">
<File Id="BinIndexJson" Source="..\..\..\..\binaries\bin-index.json" />
<File Id="PortmasterCoreExe" Source="..\..\..\..\binaries\portmaster-core.exe" />
<File Id="PortmasterKextSys" Source="..\..\..\..\binaries\portmaster-kext.sys" />
<File Id="PortmasterZip" Source="..\..\..\..\binaries\portmaster.zip" />
<File Id="AssetsZip" Source="..\..\..\..\binaries\assets.zip" />
</Component>
<Component Id="IntelFiles" Directory="IntelDir" Guid="0bb439f1-2075-45b0-95bf-78ed3dffeb69">
<File Id="IntelIndexJson" Source="..\..\..\..\binaries\intel-index.json" />
<File Id="BaseDsdl" Source="..\..\..\..\binaries\base.dsdl" />
<File Id="Geoipv4Mmdb" Source="..\..\..\..\binaries\geoipv4.mmdb" />
<File Id="Geoipv6Mmdb" Source="..\..\..\..\binaries\geoipv6.mmdb" />
<File Id="IndexDsd" Source="..\..\..\..\binaries\index.dsd" />
<File Id="IntermediateDsdl" Source="..\..\..\..\binaries\intermediate.dsdl" />
<File Id="UrgentDsdl" Source="..\..\..\..\binaries\urgent.dsdl" />
</Component>
</Fragment>
<Fragment>
<ComponentGroup Id="BinaryAndIntelFiles">
<ComponentRef Id="BinaryFiles" />
<ComponentRef Id="IntelFiles" />
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -341,14 +341,16 @@
</InstallExecuteSequence>
{{/if}}
<CustomActionRef Id='InstallPortmasterService' />
<CustomActionRef Id='StopPortmasterService' />
<CustomActionRef Id='DeletePortmasterService' />
<InstallExecuteSequence>
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
</InstallExecuteSequence>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
<!-- Service fragments -->
<CustomActionRef Id='InstallPortmasterService' />
<CustomActionRef Id='StopPortmasterService' />
<CustomActionRef Id='DeletePortmasterService' />
<!-- End Service fragments -->
</Product>
</Wix>

View File

@@ -3,7 +3,7 @@
<Fragment>
<CustomAction Id="InstallPortmasterService"
Directory="INSTALLDIR"
ExeCommand="&quot;[INSTALLDIR]portmaster-start.exe&quot; install core-service --data=&quot;[INSTALLDIR]data&quot;"
ExeCommand="sc.exe create PortmasterCore binPath= &quot;[INSTALLDIR]binary\portmaster-core.exe --data [INSTALLDIR]data&quot;"
Execute="commit"
Return="check"
Impersonate="no"

View File

@@ -3,6 +3,8 @@ package service
import (
"context"
"fmt"
"os"
"path/filepath"
go_runtime "runtime"
"sync/atomic"
"time"
@@ -35,7 +37,6 @@ import (
"github.com/safing/portmaster/service/sync"
"github.com/safing/portmaster/service/ui"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/spn/access"
"github.com/safing/portmaster/spn/cabin"
"github.com/safing/portmaster/spn/captain"
@@ -103,31 +104,54 @@ type Instance struct {
CommandLineOperation func() error
}
func getCurrentBinaryFolder() (string, error) {
// Get the path of the currently running executable
exePath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("failed to get executable path: %w", err)
}
// Get the absolute path
absPath, err := filepath.Abs(exePath)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
// Get the directory of the executable
installDir := filepath.Dir(absPath)
return installDir, nil
}
// New returns a new Portmaster service instance.
func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
var binaryUpdateIndex registry.UpdateIndex
var intelUpdateIndex registry.UpdateIndex
var binaryUpdateIndex updates.UpdateIndex
var intelUpdateIndex updates.UpdateIndex
if go_runtime.GOOS == "windows" {
binaryUpdateIndex = registry.UpdateIndex{
Directory: "C:/Program Files/Portmaster/binary",
DownloadDirectory: "C:/Program Files/Portmaster/new_binary",
PurgeDirectory: "C:/Program Files/Portmaster/old_binary",
binaryFolder, err := getCurrentBinaryFolder()
if err != nil {
return nil, err
}
binaryUpdateIndex = updates.UpdateIndex{
Directory: binaryFolder, // Default: C:/Program Files/Portmaster/binary
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,
}
intelUpdateIndex = registry.UpdateIndex{
Directory: "C:/Program Files/Portmaster/intel",
DownloadDirectory: "C:/Program Files/Portmaster/new_intel",
PurgeDirectory: "C:/Program Files/Portmaster/old_intel",
intelUpdateIndex = updates.UpdateIndex{
Directory: os.ExpandEnv("%ProgramData%/Portmaster/intel"),
DownloadDirectory: os.ExpandEnv("%ProgramData%/Portmaster/new_intel"),
PurgeDirectory: os.ExpandEnv("%ProgramData%/Portmaster/old_intel"),
IndexURLs: []string{"http://192.168.88.11:8000/test-intel.json"},
IndexFile: "intel-index.json",
AutoApply: true,
}
} else if go_runtime.GOOS == "linux" {
binaryUpdateIndex = registry.UpdateIndex{
binaryUpdateIndex = updates.UpdateIndex{
Directory: "/usr/lib/portmaster",
DownloadDirectory: "/var/lib/portmaster/new_bin",
PurgeDirectory: "/var/lib/portmaster/old_bin",
@@ -137,7 +161,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
AutoApply: false,
}
intelUpdateIndex = registry.UpdateIndex{
intelUpdateIndex = updates.UpdateIndex{
Directory: "/var/lib/portmaster/intel",
DownloadDirectory: "/var/lib/portmaster/new_intel",
PurgeDirectory: "/var/lib/portmaster/intel_bin",

View File

@@ -14,7 +14,7 @@ import (
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
)
const (
@@ -39,9 +39,9 @@ var (
filterListLock sync.RWMutex
// Updater files for tracking upgrades.
baseFile *registry.File
intermediateFile *registry.File
urgentFile *registry.File
baseFile *updates.File
intermediateFile *updates.File
urgentFile *updates.File
filterListsLoaded chan struct{}
)
@@ -77,7 +77,7 @@ func isLoaded() bool {
// processListFile opens the latest version of file and decodes it's DSDL
// content. It calls processEntry for each decoded filterlists entry.
func processListFile(ctx context.Context, filter *scopedBloom, file *registry.File) error {
func processListFile(ctx context.Context, filter *scopedBloom, file *updates.File) error {
f, err := os.Open(file.Path())
if err != nil {
return err

View File

@@ -9,7 +9,7 @@ import (
"github.com/safing/portmaster/base/database"
"github.com/safing/portmaster/base/database/record"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
"github.com/safing/structures/dsd"
)
@@ -162,7 +162,7 @@ func getListIndexFromCache() (*ListIndexFile, error) {
var (
// listIndexUpdate must only be used by updateListIndex.
listIndexUpdate *registry.File
listIndexUpdate *updates.File
listIndexUpdateLock sync.Mutex
)

View File

@@ -14,7 +14,7 @@ import (
"github.com/safing/portmaster/base/database/query"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
)
var updateInProgress = abool.New()
@@ -174,8 +174,8 @@ func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
// getUpgradableFiles returns a slice of filterlists files
// that should be updated. The files MUST be updated and
// processed in the returned order!
func getUpgradableFiles() ([]*registry.File, error) {
var updateOrder []*registry.File
func getUpgradableFiles() ([]*updates.File, error) {
var updateOrder []*updates.File
// cacheDBInUse := isLoaded()
@@ -218,7 +218,7 @@ func getUpgradableFiles() ([]*registry.File, error) {
return resolveUpdateOrder(updateOrder)
}
func resolveUpdateOrder(updateOrder []*registry.File) ([]*registry.File, error) {
func resolveUpdateOrder(updateOrder []*updates.File) ([]*updates.File, error) {
// sort the update order by ascending version
sort.Sort(byAscVersion(updateOrder))
log.Tracef("intel/filterlists: order of updates: %v", updateOrder)
@@ -258,7 +258,7 @@ func resolveUpdateOrder(updateOrder []*registry.File) ([]*registry.File, error)
return updateOrder[startAtIdx:], nil
}
type byAscVersion []*registry.File
type byAscVersion []*updates.File
func (fs byAscVersion) Len() int { return len(fs) }

View File

@@ -9,7 +9,7 @@ import (
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
)
var worker *updateWorker
@@ -27,7 +27,7 @@ const (
type geoIPDB struct {
*maxminddb.Reader
file *registry.File
file *updates.File
}
// updateBroadcaster stores a geoIPDB and provides synchronized
@@ -197,7 +197,7 @@ func getGeoIPDB(resource string) (*geoIPDB, error) {
}, nil
}
func open(resource string) (*registry.File, error) {
func open(resource string) (*updates.File, error) {
f, err := module.instance.IntelUpdates().GetFile(resource)
if err != nil {
return nil, fmt.Errorf("getting file: %w", err)

View File

@@ -16,7 +16,7 @@ import (
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
)
var (
@@ -93,7 +93,7 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get file from update system
zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName))
if err != nil {
if errors.Is(err, registry.ErrNotFound) {
if errors.Is(err, updates.ErrNotFound) {
log.Tracef("ui: requested module %s does not exist", moduleName)
http.Error(w, err.Error(), http.StatusNotFound)
} else {

View File

@@ -1,4 +1,4 @@
package registry
package updates
import (
"archive/zip"
@@ -6,6 +6,7 @@ import (
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
@@ -20,7 +21,7 @@ import (
const MaxUnpackSize = 1 << 30 // 2^30 == 1GB
const current_platform = runtime.GOOS + "_" + runtime.GOARCH
const currentPlatform = runtime.GOOS + "_" + runtime.GOARCH
type Artifact struct {
Filename string `json:"Filename"`
@@ -38,7 +39,73 @@ type Bundle struct {
Artifacts []Artifact `json:"Artifacts"`
}
func (bundle Bundle) downloadAndVerify(dir string) {
func ParseBundle(dir string, indexFile string) (*Bundle, error) {
filepath := fmt.Sprintf("%s/%s", dir, indexFile)
// Check if the file exists.
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open index file: %w", err)
}
defer func() { _ = file.Close() }()
// Read
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
// Parse
var bundle Bundle
err = json.Unmarshal(content, &bundle)
if err != nil {
return nil, err
}
return &bundle, nil
}
// CopyMatchingFilesFromCurrent check if there the current bundle files has matching files with the new bundle and copies them if they match.
func (bundle Bundle) CopyMatchingFilesFromCurrent(current Bundle, currentDir, newDir string) error {
for _, currentArtifact := range current.Artifacts {
new:
for _, newArtifact := range bundle.Artifacts {
if currentArtifact.Filename == newArtifact.Filename {
if currentArtifact.SHA256 == newArtifact.SHA256 {
// Files match, make sure new dir exists
_ = os.MkdirAll(newDir, defaultDirMode)
// Open the current file
sourceFilePath := fmt.Sprintf("%s/%s", currentDir, newArtifact.Filename)
sourceFile, err := os.Open(sourceFilePath)
if err != nil {
return fmt.Errorf("failed to open %s file: %w", sourceFilePath, err)
}
defer sourceFile.Close()
// Create new file
destFilePath := fmt.Sprintf("%s/%s", newDir, newArtifact.Filename)
destFile, err := os.Create(destFilePath)
if err != nil {
return fmt.Errorf("failed to create %s file: %w", destFilePath, err)
}
defer destFile.Close()
// Copy
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return fmt.Errorf("failed to copy contents: %w", err)
}
// Flush
_ = destFile.Sync()
}
break new
}
}
}
return nil
}
func (bundle Bundle) DownloadAndVerify(dir string) {
client := http.Client{}
for _, artifact := range bundle.Artifacts {
@@ -111,7 +178,7 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) {
func processArtifact(client *http.Client, artifact Artifact, filePath string) error {
// Skip artifacts not meant for this machine.
if artifact.Platform != "" && artifact.Platform != current_platform {
if artifact.Platform != "" && artifact.Platform != currentPlatform {
return nil
}

View File

@@ -1,4 +1,4 @@
package registry
package updates
import (
"fmt"
@@ -19,7 +19,7 @@ type UpdateIndex struct {
AutoApply bool
}
func (ui *UpdateIndex) downloadIndexFile() (err error) {
func (ui *UpdateIndex) DownloadIndexFile() (err error) {
_ = os.MkdirAll(ui.DownloadDirectory, defaultDirMode)
for _, url := range ui.IndexURLs {
err = ui.downloadIndexFileFromURL(url)

View File

@@ -1,23 +1,38 @@
package updates
import (
"flag"
"errors"
"fmt"
"os"
"time"
semver "github.com/hashicorp/go-version"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
)
var autoUpdate bool
func init() {
flag.BoolVar(&autoUpdate, "auto-update", false, "auto apply downloaded updates")
type File struct {
id string
path string
}
func (f *File) Identifier() string {
return f.id
}
func (f *File) Path() string {
return f.path
}
func (f *File) Version() string {
return ""
}
var ErrNotFound error = errors.New("file not found")
// Updates provides access to released artifacts.
type Updates struct {
m *mgr.Manager
@@ -29,13 +44,18 @@ type Updates struct {
EventResourcesUpdated *mgr.EventMgr[struct{}]
EventVersionsUpdated *mgr.EventMgr[struct{}]
registry registry.Registry
updateIndex UpdateIndex
bundle *Bundle
updateBundle *Bundle
files map[string]File
instance instance
}
// New returns a new Updates module.
func New(instance instance, name string, index registry.UpdateIndex) (*Updates, error) {
func New(instance instance, name string, index UpdateIndex) (*Updates, error) {
m := mgr.New(name)
module := &Updates{
m: m,
@@ -44,6 +64,8 @@ func New(instance instance, name string, index registry.UpdateIndex) (*Updates,
EventResourcesUpdated: mgr.NewEventMgr[struct{}](ResourceUpdateEvent, m),
EventVersionsUpdated: mgr.NewEventMgr[struct{}](VersionUpdateEvent, m),
updateIndex: index,
instance: instance,
}
@@ -51,39 +73,87 @@ func New(instance instance, name string, index registry.UpdateIndex) (*Updates,
module.updateCheckWorkerMgr = m.NewWorkerMgr("update checker", module.checkForUpdates, nil)
module.updateCheckWorkerMgr.Repeat(30 * time.Second)
module.upgraderWorkerMgr = m.NewWorkerMgr("upgrader", func(w *mgr.WorkerCtx) error {
err := module.registry.ApplyUpdates()
err := applyUpdates(module.updateIndex, *module.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?
module.instance.Restart()
}
return nil
}, nil)
module.registry = registry.New(index)
_ = module.registry.Initialize()
var err error
module.bundle, err = ParseBundle(module.updateIndex.Directory, module.updateIndex.IndexFile)
if err != nil {
return nil, fmt.Errorf("failed to parse binary bundle: %s", err)
}
// Add bundle artifacts to registry.
module.processBundle(module.bundle)
// Remove old files
m.Go("old files cleaner", func(ctx *mgr.WorkerCtx) error {
err := os.RemoveAll(module.updateIndex.PurgeDirectory)
if err != nil {
return fmt.Errorf("failed to delete folder: %w", err)
}
return nil
})
return module, nil
}
func (reg *Updates) processBundle(bundle *Bundle) {
for _, artifact := range bundle.Artifacts {
artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename)
reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath}
}
}
func (u *Updates) checkForUpdates(_ *mgr.WorkerCtx) error {
hasUpdates, err := u.registry.CheckForUpdates()
err := u.updateIndex.DownloadIndexFile()
if err != nil {
log.Errorf("updates: failed to check for updates: %s", err)
return fmt.Errorf("failed to download index file: %s", err)
}
if hasUpdates {
log.Infof("updates: there is updates available")
err = u.registry.DownloadUpdates()
if err != nil {
log.Errorf("updates: failed to download bundle: %s", err)
} else if autoUpdate {
u.ApplyUpdates()
}
} else {
log.Infof("updates: no new updates")
u.EventResourcesUpdated.Submit(struct{}{})
u.updateBundle, err = ParseBundle(u.updateIndex.DownloadDirectory, u.updateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed parse bundle: %s", err)
}
defer u.EventResourcesUpdated.Submit(struct{}{})
// Compare current and downloaded index version.
currentVersion, err := semver.NewVersion(u.bundle.Version)
downloadVersion, err := semver.NewVersion(u.updateBundle.Version)
if currentVersion.Compare(downloadVersion) <= 0 {
// no updates
log.Info("updates: check complete: no new updates")
return nil
}
log.Infof("updates: check complete: downloading new version: %s %s", u.updateBundle.Name, u.updateBundle.Version)
err = u.DownloadUpdates()
if err != nil {
log.Errorf("updates: failed to download bundle: %s", err)
} else if u.updateIndex.AutoApply {
u.ApplyUpdates()
}
return nil
}
// DownloadUpdates downloads available binary updates.
func (u *Updates) DownloadUpdates() error {
if u.updateBundle == nil {
// CheckForBinaryUpdates needs to be called before this.
return fmt.Errorf("no valid update bundle found")
}
_ = deleteUnfinishedDownloads(u.updateIndex.DownloadDirectory)
err := u.updateBundle.CopyMatchingFilesFromCurrent(*u.bundle, u.updateIndex.Directory, u.updateIndex.DownloadDirectory)
if err != nil {
log.Warningf("updates: error while coping file from current to update: %s", err)
}
u.updateBundle.DownloadAndVerify(u.updateIndex.DownloadDirectory)
return nil
}
@@ -107,8 +177,17 @@ func (u *Updates) Start() error {
return nil
}
func (u *Updates) GetFile(id string) (*registry.File, error) {
return u.registry.GetFile(id)
func (u *Updates) GetFile(id string) (*File, error) {
file, ok := u.files[id]
if ok {
return &file, nil
} else {
log.Errorf("updates: requested file id not found: %s", id)
for _, file := range u.files {
log.Debugf("File: %s", file)
}
return nil, ErrNotFound
}
}
// Stop stops the module.

View File

@@ -1,210 +0,0 @@
package registry
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/safing/portmaster/base/log"
)
var ErrNotFound error = errors.New("file not found")
const (
defaultFileMode = os.FileMode(0o0644)
executableFileMode = os.FileMode(0o0744)
defaultDirMode = os.FileMode(0o0755)
)
type File struct {
id string
path string
}
func (f *File) Identifier() string {
return f.id
}
func (f *File) Path() string {
return f.path
}
func (f *File) Version() string {
return ""
}
type Registry struct {
updateIndex UpdateIndex
bundle *Bundle
updateBundle *Bundle
files map[string]File
}
// New create new Registry.
func New(index UpdateIndex) Registry {
return Registry{
updateIndex: index,
files: make(map[string]File),
}
}
// Initialize parses and initializes currently installed bundles.
func (reg *Registry) Initialize() error {
var err error
// Parse current installed binary bundle.
reg.bundle, err = parseBundle(reg.updateIndex.Directory, reg.updateIndex.IndexFile)
if err != nil {
return fmt.Errorf("failed to parse binary bundle: %w", err)
}
// Add bundle artifacts to registry.
reg.processBundle(reg.bundle)
return nil
}
func (reg *Registry) processBundle(bundle *Bundle) {
for _, artifact := range bundle.Artifacts {
artifactPath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename)
reg.files[artifact.Filename] = File{id: artifact.Filename, path: artifactPath}
}
}
// GetFile returns the object of a artifact by id.
func (reg *Registry) GetFile(id string) (*File, error) {
file, ok := reg.files[id]
if ok {
return &file, nil
} else {
log.Errorf("updates: requested file id not found: %s", id)
for _, file := range reg.files {
log.Debugf("File: %s", file)
}
return nil, ErrNotFound
}
}
// CheckForUpdates checks if there is a new binary bundle updates.
func (reg *Registry) CheckForUpdates() (bool, error) {
err := reg.updateIndex.downloadIndexFile()
if err != nil {
return false, err
}
reg.updateBundle, err = parseBundle(reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile)
if err != nil {
return false, err
}
// TODO(vladimir): Make a better check.
if reg.bundle.Version != reg.updateBundle.Version {
return true, nil
}
return false, nil
}
// DownloadUpdates downloads available binary updates.
func (reg *Registry) DownloadUpdates() error {
if reg.updateBundle == nil {
// CheckForBinaryUpdates needs to be called before this.
return fmt.Errorf("no valid update bundle found")
}
_ = deleteUnfinishedDownloads(reg.updateIndex.DownloadDirectory)
reg.updateBundle.downloadAndVerify(reg.updateIndex.DownloadDirectory)
return nil
}
// ApplyUpdates removes the current binary folder and replaces it with the downloaded one.
func (reg *Registry) ApplyUpdates() error {
// Create purge dir.
err := os.MkdirAll(reg.updateIndex.PurgeDirectory, defaultDirMode)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Read all files in the current version folder.
files, err := os.ReadDir(reg.updateIndex.Directory)
if err != nil {
return err
}
// Move current version files into purge folder.
for _, file := range files {
filepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, file.Name())
purgePath := fmt.Sprintf("%s/%s", reg.updateIndex.PurgeDirectory, file.Name())
err := os.Rename(filepath, purgePath)
if err != nil {
return fmt.Errorf("failed to move file %s: %w", filepath, err)
}
}
// Move the new index file
indexFile := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, reg.updateIndex.IndexFile)
newIndexFile := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, reg.updateIndex.IndexFile)
err = os.Rename(indexFile, newIndexFile)
if err != nil {
return fmt.Errorf("failed to move index file %s: %w", indexFile, err)
}
// Move downloaded files to the current version folder.
for _, artifact := range reg.bundle.Artifacts {
fromFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.DownloadDirectory, artifact.Filename)
toFilepath := fmt.Sprintf("%s/%s", reg.updateIndex.Directory, artifact.Filename)
err = os.Rename(fromFilepath, toFilepath)
if err != nil {
return fmt.Errorf("failed to move file %s: %w", fromFilepath, err)
}
}
return nil
}
func parseBundle(dir string, indexFile string) (*Bundle, error) {
filepath := fmt.Sprintf("%s/%s", dir, indexFile)
// Check if the file exists.
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open index file: %w", err)
}
defer func() { _ = file.Close() }()
// Read
content, err := io.ReadAll(file)
if err != nil {
return nil, err
}
// Parse
var bundle Bundle
err = json.Unmarshal(content, &bundle)
if err != nil {
return nil, err
}
return &bundle, nil
}
func deleteUnfinishedDownloads(rootDir string) error {
return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if the current file has the specified extension
if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") {
log.Warningf("updates: deleting unfinished: %s\n", path)
err := os.Remove(path)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", path, err)
}
}
return nil
})
}

View File

@@ -0,0 +1,78 @@
package updates
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safing/portmaster/base/log"
)
const (
defaultFileMode = os.FileMode(0o0644)
executableFileMode = os.FileMode(0o0744)
defaultDirMode = os.FileMode(0o0755)
)
func applyUpdates(updateIndex UpdateIndex, newBundle Bundle) error {
// Create purge dir.
err := os.MkdirAll(updateIndex.PurgeDirectory, defaultDirMode)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Read all files in the current version folder.
files, err := os.ReadDir(updateIndex.Directory)
if err != nil {
return err
}
// Move current version files into purge folder.
for _, file := range files {
filepath := fmt.Sprintf("%s/%s", updateIndex.Directory, file.Name())
purgePath := fmt.Sprintf("%s/%s", updateIndex.PurgeDirectory, file.Name())
err := os.Rename(filepath, purgePath)
if err != nil {
return fmt.Errorf("failed to move file %s: %w", filepath, err)
}
}
// Move the new index file
indexFile := fmt.Sprintf("%s/%s", updateIndex.DownloadDirectory, updateIndex.IndexFile)
newIndexFile := fmt.Sprintf("%s/%s", updateIndex.Directory, updateIndex.IndexFile)
err = os.Rename(indexFile, newIndexFile)
if err != nil {
return fmt.Errorf("failed to move index file %s: %w", indexFile, err)
}
// Move downloaded files to the current version folder.
for _, artifact := range newBundle.Artifacts {
fromFilepath := fmt.Sprintf("%s/%s", updateIndex.DownloadDirectory, artifact.Filename)
toFilepath := fmt.Sprintf("%s/%s", updateIndex.Directory, artifact.Filename)
err = os.Rename(fromFilepath, toFilepath)
if err != nil {
return fmt.Errorf("failed to move file %s: %w", fromFilepath, err)
}
}
return nil
}
func deleteUnfinishedDownloads(rootDir string) error {
return filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if the current file has the specified extension
if !info.IsDir() && strings.HasSuffix(info.Name(), ".download") {
log.Warningf("updates: deleting unfinished: %s\n", path)
err := os.Remove(path)
if err != nil {
return fmt.Errorf("failed to delete file %s: %w", path, err)
}
}
return nil
})
}

View File

@@ -7,7 +7,7 @@ import (
"sync"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates/registry"
"github.com/safing/portmaster/service/updates"
"github.com/safing/portmaster/spn/conf"
"github.com/safing/portmaster/spn/hub"
"github.com/safing/portmaster/spn/navigator"
@@ -15,7 +15,7 @@ import (
)
var (
intelResource *registry.File
intelResource *updates.File
intelResourcePath = "intel/spn/main-intel.yaml"
intelResourceMapName = "main"
intelResourceUpdateLock sync.Mutex