From d70edcf18428b0382cb5f1421c8037cc4e402854 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 26 Jul 2019 22:34:34 +0200 Subject: [PATCH] Add upgrader for portmaster-control --- updates/updater.go | 15 ++++ updates/upgrader.go | 187 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 updates/upgrader.go diff --git a/updates/updater.go b/updates/updater.go index aaaf3843..9ef17788 100644 --- a/updates/updater.go +++ b/updates/updater.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "os" "path" "path/filepath" "runtime" @@ -25,6 +26,14 @@ func updater() { 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) } } @@ -143,5 +152,11 @@ func DownloadUpdates() (err error) { } log.Tracef("updates: finished updating existing files") + // remove tmp folder after we are finished + err = os.RemoveAll(updateStoragePath) + if err != nil { + log.Tracef("updates: failed to remove tmp dir %s after downloading updates: %s", updateStoragePath, err) + } + return nil } diff --git a/updates/upgrader.go b/updates/upgrader.go new file mode 100644 index 00000000..6955efeb --- /dev/null +++ b/updates/upgrader.go @@ -0,0 +1,187 @@ +package updates + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/google/renameio" + "github.com/safing/portbase/utils" + + "github.com/safing/portbase/log" + + processInfo "github.com/shirou/gopsutil/process" +) + +const ( + upgradedSuffix = "-upgraded" +) + +func runFileUpgrades() error { + filename := "portmaster-control" + if runtime.GOOS == "windows" { + filename += ".exe" + } + + // get newest portmaster-control + newFile, err := GetPlatformFile("control/" + filename) // identifier, use forward slash! + if err != nil { + return err + } + + // update portmaster-control in data root + rootControlPath := filepath.Join(filepath.Dir(updateStoragePath), filename) + err = upgradeFile(rootControlPath, newFile) + if err != nil { + return err + } + log.Infof("updates: upgraded %s", rootControlPath) + + // upgrade parent process, if it's portmaster-control + parent, err := processInfo.NewProcess(int32(os.Getppid())) + if err != nil { + return fmt.Errorf("could not get parent process for upgrade checks: %s", err) + } + parentName, err := parent.Name() + if err != nil { + return fmt.Errorf("could not get parent process name for upgrade checks: %s", err) + } + if !strings.HasPrefix(parentName, filename) { + log.Tracef("updates: parent process does not seem to be portmaster-control, name is %s", parentName) + return nil + } + parentPath, err := parent.Exe() + if err != nil { + return fmt.Errorf("could not get parent process path for upgrade: %s", err) + } + err = upgradeFile(parentPath, newFile) + if err != nil { + return err + } + log.Infof("updates: upgraded %s", parentPath) + + return nil +} + +func upgradeFile(fileToUpgrade string, file *File) error { + fileExists := false + _, err := os.Stat(fileToUpgrade) + if err == nil { + // file exists and is accessible + fileExists = true + } + + if fileExists { + // get current version + var currentVersion string + cmd := exec.Command(fileToUpgrade, "--ver") + out, err := cmd.Output() + if err == nil { + // abort if version matches + currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*") + if currentVersion == file.Version() { + // already up to date! + return nil + } + } else { + log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err) + currentVersion = "0.0.0" + } + + // test currentVersion for sanity + if !rawVersionRegex.MatchString(currentVersion) { + log.Tracef("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion) + currentVersion = "0.0.0" + } + + // try removing old version + err = os.Remove(fileToUpgrade) + if err != nil { + // maybe we're on windows and it's in use, try moving + // create dir + err = utils.EnsureDirectory(downloadTmpPath, 0700) + if err != nil { + return fmt.Errorf("unable to create directory for upgrade process: %s", err) + } + // move + err = os.Rename(fileToUpgrade, filepath.Join( + downloadTmpPath, + fmt.Sprintf( + "%s-%d%s", + GetVersionedPath(filepath.Base(fileToUpgrade), currentVersion), + time.Now().UTC().Unix(), + upgradedSuffix, + ), + )) + if err != nil { + return fmt.Errorf("unable to move file that needs upgrade: %s", err) + } + } + } + + // copy upgrade + // TODO: handle copy failure + err = copyFile(file.Path(), fileToUpgrade) + if err != nil { + time.Sleep(1 * time.Second) + // try again + err = copyFile(file.Path(), fileToUpgrade) + if err != nil { + return err + } + } + + // check permissions + if runtime.GOOS != "windows" { + info, err := os.Stat(fileToUpgrade) + if err != nil { + return fmt.Errorf("failed to get file info on %s: %s", fileToUpgrade, err) + } + if info.Mode() != 0755 { + err := os.Chmod(fileToUpgrade, 0755) + if err != nil { + return fmt.Errorf("failed to set permissions on %s: %s", fileToUpgrade, err) + } + } + } + return nil +} + +func copyFile(srcPath, dstPath string) (err error) { + // open file for writing + atomicDstFile, err := renameio.TempFile(downloadTmpPath, dstPath) + if err != nil { + return fmt.Errorf("could not create temp file for atomic copy: %s", err) + } + defer atomicDstFile.Cleanup() + + // open source + srcFile, err := os.Open(srcPath) + if err != nil { + return + } + defer srcFile.Close() + + // copy data + _, err = io.Copy(atomicDstFile, srcFile) + if err != nil { + return + } + + // finalize file + err = atomicDstFile.CloseAtomicallyReplace() + if err != nil { + return fmt.Errorf("updates: failed to finalize copy to file %s: %s", dstPath, err) + } + + return nil +} + +func cleanOldUpgradedFiles() error { + return os.RemoveAll(downloadTmpPath) +}