[WIP] Fix edge upgrade edge cases

This commit is contained in:
Vladimir Stoilov 2024-09-25 16:33:45 +03:00
parent 89b533f949
commit 1b6ee722f3
No known key found for this signature in database
GPG key ID: 2F190B67A43A81AF
6 changed files with 124 additions and 29 deletions

View file

@ -1,14 +1,17 @@
<?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 Id="TARGETDIR">
<Directory Id="CommonAppDataFolder" Name="CommonAppData">
<Directory Id="PortmasterDir" Name="Portmaster">
<Directory Id="IntelDir" Name="intel" />
</Directory>
</Directory>
</DirectoryRef>
</Fragment>
<Fragment>
<Component Id="BinaryFiles" Directory="BinaryDir" Guid="850cdd31-424d-45f5-b8f0-95df950ebd0d">
<Component Id="BinaryFiles" Directory="INSTALLDIR" 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" />

View file

@ -1,13 +1,34 @@
!define NSIS_HOOK_POSTINSTALL "NSIS_HOOK_POSTINSTALL_"
!macro NSIS_HOOK_PREINSTALL
; Current working directory is <project-dir>\target\release\nsis\x64
SetOutPath "$INSTDIR"
File "..\..\..\..\binaries\bin-index.json"
File "..\..\..\..\binaries\portmaster-core.exe"
File "..\..\..\..\binaries\portmaster-kext.sys"
File "..\..\..\..\binaries\portmaster.zip"
File "..\..\..\..\binaries\assets.zip"
SetOutPath "$COMMONPROGRAMDATA\Portmaster\intel"
File "..\..\..\..\binaries\intel-index.json"
File "..\..\..\..\binaries\base.dsdl"
File "..\..\..\..\binaries\geoipv4.mmdb"
File "..\..\..\..\binaries\geoipv6.mmdb"
File "..\..\..\..\binaries\index.dsd"
File "..\..\..\..\binaries\intermediate.dsdl"
File "..\..\..\..\binaries\urgent.dsdl"
; restire previous state
SetOutPath "$INSTDIR"
!macro NSIS_HOOK_POSTINSTALL_
ExecWait '"$INSTDIR\portmaster-start.exe" install core-service --data="$INSTDIR\data"'
!macroend
!macro NSIS_HOOK_POSTINSTALL
ExecWait 'sc.exe create PortmasterCore binPath= "$INSTDIR\portmaster-core.exe" --data="$COMMONPROGRAMDATA\Portmaster\data"'
!macroend
!define NSIS_HOOK_PREUNINSTALL "NSIS_HOOK_PREUNINSTALL_"
!macro NSIS_HOOK_PREUNINSTALL_
!macro NSIS_HOOK_PREUNINSTALL
ExecWait 'sc.exe stop PortmasterCore'
ExecWait 'sc.exe delete PortmasterCore'
!macroend

View file

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

View file

@ -134,8 +134,8 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
}
binaryUpdateIndex = updates.UpdateIndex{
Directory: binaryFolder, // Default: C:/Program Files/Portmaster
DownloadDirectory: os.ExpandEnv("%ProgramData%/Portmaster/new_binary"),
PurgeDirectory: os.ExpandEnv("%ProgramData%/Portmaster/old_binary"),
DownloadDirectory: os.ExpandEnv("$ProgramData/Portmaster/new_binary"),
PurgeDirectory: filepath.Join(binaryFolder, "old_binary"), // Default: C:/Program Files/Portmaster/old_binary
Ignore: []string{"databases", "intel", "config.json"},
IndexURLs: []string{"http://192.168.88.11:8000/test-binary.json"},
IndexFile: "bin-index.json",
@ -144,9 +144,9 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
}
intelUpdateIndex = updates.UpdateIndex{
Directory: os.ExpandEnv("%ProgramData%/Portmaster/intel"),
DownloadDirectory: os.ExpandEnv("%ProgramData%/Portmaster/new_intel"),
PurgeDirectory: os.ExpandEnv("%ProgramData%/Portmaster/old_intel"),
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,

View file

@ -36,7 +36,7 @@ const (
// fetching resources from the update server.
var UserAgent = fmt.Sprintf("Portmaster (%s %s)", runtime.GOOS, runtime.GOARCH)
// UpdateIndex holds the configuration for the updates module
// UpdateIndex holds the configuration for the updates module.
type UpdateIndex struct {
Directory string
DownloadDirectory string
@ -143,7 +143,7 @@ func (u *Updates) applyUpdates(_ *mgr.WorkerCtx) error {
currentBundle := u.registry.bundle
downloadBundle := u.downloader.bundle
log.Infof("update: starting update: %s %s -> %s", currentBundle.Name, currentBundle.Version, downloadBundle.Version)
err := u.registry.performUpgrade(u.downloader.dir, u.downloader.indexFile)
err := u.registry.performRecoverableUpgrade(u.downloader.dir, u.downloader.indexFile)
if err != nil {
// TODO(vladimir): Send notification to UI
log.Errorf("updates: failed to apply updates: %s", err)

View file

@ -3,6 +3,7 @@ package updates
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
@ -67,11 +68,8 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error {
return fmt.Errorf("invalid update: %w", err)
}
// Create purge dir.
err = os.MkdirAll(r.purgeDir, defaultDirMode)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Make sure purge dir is empty.
_ = os.RemoveAll(r.purgeDir)
// Read all files in the current version folder.
files, err := os.ReadDir(r.dir)
@ -79,12 +77,18 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error {
return err
}
// Create purge dir. Calling this after ReadDIr is important.
err = os.MkdirAll(r.purgeDir, defaultDirMode)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Move current version files into purge folder.
log.Debugf("updates: removing the old version")
for _, file := range files {
currentFilepath := filepath.Join(r.dir, file.Name())
purgePath := filepath.Join(r.purgeDir, file.Name())
err := os.Rename(currentFilepath, purgePath)
err := moveFile(currentFilepath, purgePath)
if err != nil {
return fmt.Errorf("failed to move file %s: %w", currentFilepath, err)
}
@ -93,7 +97,7 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error {
// Move the new index file
log.Debugf("updates: installing the new version")
newIndexFile := filepath.Join(r.dir, indexFile)
err = os.Rename(indexFilepath, newIndexFile)
err = moveFile(indexFilepath, newIndexFile)
if err != nil {
return fmt.Errorf("failed to move index file %s: %w", indexFile, err)
}
@ -102,9 +106,9 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error {
for _, artifact := range bundle.Artifacts {
fromFilepath := filepath.Join(downloadDir, artifact.Filename)
toFilepath := filepath.Join(r.dir, artifact.Filename)
err = os.Rename(fromFilepath, toFilepath)
err = moveFile(fromFilepath, toFilepath)
if err != nil {
log.Errorf("failed to move file %s: %s", fromFilepath, err)
return fmt.Errorf("failed to move file %s: %w", fromFilepath, err)
} else {
log.Debugf("updates: %s moved", artifact.Filename)
}
@ -125,9 +129,76 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error {
log.Infof("updates: update complete")
err = r.CleanOldFiles()
return nil
}
func moveFile(currentPath, newPath string) error {
err := os.Rename(currentPath, newPath)
if err == nil {
// Moving was successful return
return nil
}
log.Debugf("updates: failed to move '%s' fallback to copy+delete: %s -> %s", err, currentPath, newPath)
// Failed to move, try copy and delete
currentFile, err := os.Open(currentPath)
if err != nil {
log.Warningf("updates: error while cleaning old file: %s", err)
return err
}
defer func() { _ = currentFile.Close() }()
newFile, err := os.Create(newPath)
if err != nil {
return err
}
defer func() { _ = newFile.Close() }()
_, err = io.Copy(newFile, currentFile)
if err != nil {
return err
}
// Make sure file is closed before deletion.
_ = currentFile.Close()
currentFile = nil
err = os.Remove(currentPath)
if err != nil {
log.Errorf("updates: failed to delete while moving file: %s", err)
}
return nil
}
func (r *Registry) performRecoverableUpgrade(downloadDir string, indexFile string) error {
upgradeError := r.performUpgrade(downloadDir, indexFile)
if upgradeError != nil {
err := r.recover()
recoverStatus := "(recovery successful)"
if err != nil {
recoverStatus = "(recovery failed)"
log.Errorf("updates: failed to recover: %s", err)
}
return fmt.Errorf("upgrade failed: %w %s", upgradeError, recoverStatus)
}
return nil
}
func (r *Registry) recover() error {
files, err := os.ReadDir(r.purgeDir)
if err != nil {
return err
}
for _, file := range files {
recoverPath := filepath.Join(r.purgeDir, file.Name())
currentFilepath := filepath.Join(r.dir, file.Name())
err := moveFile(recoverPath, currentFilepath)
if err != nil {
return err
}
}
return nil