mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
386 lines
9.1 KiB
Go
386 lines
9.1 KiB
Go
package updates
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/renameio"
|
|
processInfo "github.com/shirou/gopsutil/process"
|
|
"github.com/tevino/abool"
|
|
|
|
"github.com/safing/portbase/dataroot"
|
|
"github.com/safing/portbase/info"
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/notifications"
|
|
"github.com/safing/portbase/rng"
|
|
"github.com/safing/portbase/updater"
|
|
)
|
|
|
|
const (
|
|
upgradedSuffix = "-upgraded"
|
|
exeExt = ".exe"
|
|
)
|
|
|
|
var (
|
|
upgraderActive = abool.NewBool(false)
|
|
|
|
pmCtrlUpdate *updater.File
|
|
pmCoreUpdate *updater.File
|
|
|
|
spnHubUpdate *updater.File
|
|
hubUpgradeStarted bool
|
|
|
|
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
|
|
)
|
|
|
|
func initUpgrader() error {
|
|
return module.RegisterEventHook(
|
|
ModuleName,
|
|
ResourceUpdateEvent,
|
|
"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-start
|
|
err := upgradePortmasterStart()
|
|
if err != nil {
|
|
log.Warningf("updates: failed to upgrade portmaster-start: %s", err)
|
|
}
|
|
|
|
binBaseName := strings.Split(filepath.Base(os.Args[0]), "_")[0]
|
|
switch binBaseName {
|
|
case "portmaster-core":
|
|
err = upgradeCoreNotify()
|
|
if err != nil {
|
|
log.Warningf("updates: failed to notify about core upgrade: %s", err)
|
|
}
|
|
|
|
case "spn-hub":
|
|
err = upgradeHub()
|
|
if err != nil {
|
|
log.Warningf("updates: failed to initiate hub upgrade: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upgradeCoreNotify() error {
|
|
if pmCoreUpdate != nil && !pmCoreUpdate.UpgradeAvailable() {
|
|
return nil
|
|
}
|
|
|
|
// make identifier
|
|
identifier := "core/portmaster-core" // identifier, use forward slash!
|
|
if onWindows {
|
|
identifier += exeExt
|
|
}
|
|
|
|
// get newest portmaster-core
|
|
new, err := GetPlatformFile(identifier)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pmCoreUpdate = new
|
|
|
|
// check for new version
|
|
if info.GetInfo().Version != pmCoreUpdate.Version() {
|
|
n := notifications.Notify(¬ifications.Notification{
|
|
EventID: "updates:core-update-available",
|
|
Type: notifications.Info,
|
|
Title: fmt.Sprintf(
|
|
"Portmaster Update v%s",
|
|
pmCoreUpdate.Version(),
|
|
),
|
|
Category: "Core",
|
|
Message: fmt.Sprintf(
|
|
`A new Portmaster version is available! Restart the Portmaster to upgrade to %s.`,
|
|
pmCoreUpdate.Version(),
|
|
),
|
|
ShowOnSystem: true,
|
|
AvailableActions: []*notifications.Action{
|
|
{
|
|
ID: "restart",
|
|
Text: "Restart",
|
|
},
|
|
{
|
|
ID: "later",
|
|
Text: "Not now",
|
|
},
|
|
},
|
|
})
|
|
n.SetActionFunction(upgradeCoreNotifyActionHandler)
|
|
|
|
log.Debugf("updates: new portmaster version available, sending notification to user")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
|
|
switch n.SelectedActionID {
|
|
case "restart":
|
|
log.Infof("updates: user triggered restart via core update notification")
|
|
RestartNow()
|
|
case "later":
|
|
n.Delete()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upgradeHub() error {
|
|
if hubUpgradeStarted {
|
|
return nil
|
|
}
|
|
if spnHubUpdate != nil && !spnHubUpdate.UpgradeAvailable() {
|
|
return nil
|
|
}
|
|
|
|
// make identifier
|
|
identifier := "hub/spn-hub" // identifier, use forward slash!
|
|
if onWindows {
|
|
identifier += exeExt
|
|
}
|
|
|
|
// get newest spn-hub
|
|
new, err := GetPlatformFile(identifier)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spnHubUpdate = new
|
|
|
|
// check for new version
|
|
if info.GetInfo().Version != spnHubUpdate.Version() {
|
|
// get random delay with up to three hours
|
|
delayMinutes, err := rng.Number(3 * 60)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
DelayedRestart(time.Duration(delayMinutes) * time.Minute)
|
|
hubUpgradeStarted = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upgradePortmasterStart() error {
|
|
filename := "portmaster-start"
|
|
if onWindows {
|
|
filename += exeExt
|
|
}
|
|
|
|
// check if we can upgrade
|
|
if pmCtrlUpdate == nil || pmCtrlUpdate.UpgradeAvailable() {
|
|
// get newest portmaster-start
|
|
new, err := GetPlatformFile("start/" + filename) // identifier, use forward slash!
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pmCtrlUpdate = new
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
// update portmaster-start in data root
|
|
rootPmStartPath := filepath.Join(dataroot.Root().Path, filename)
|
|
err := upgradeFile(rootPmStartPath, pmCtrlUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func warnOnIncorrectParentPath() {
|
|
expectedFileName := "portmaster-start"
|
|
if onWindows {
|
|
expectedFileName += exeExt
|
|
}
|
|
|
|
// upgrade parent process, if it's portmaster-start
|
|
parent, err := processInfo.NewProcess(int32(os.Getppid()))
|
|
if err != nil {
|
|
log.Tracef("could not get parent process: %s", err)
|
|
return
|
|
}
|
|
parentName, err := parent.Name()
|
|
if err != nil {
|
|
log.Tracef("could not get parent process name: %s", err)
|
|
return
|
|
}
|
|
if parentName != expectedFileName {
|
|
log.Warningf("updates: parent process does not seem to be portmaster-start, name is %s", parentName)
|
|
|
|
// TODO(ppacher): once we released a new installer and folks had time
|
|
// to update we should send a module warning/hint to the
|
|
// UI notifying the user that he's still using portmaster-control.
|
|
return
|
|
}
|
|
|
|
parentPath, err := parent.Exe()
|
|
if err != nil {
|
|
log.Tracef("could not get parent process path: %s", err)
|
|
return
|
|
}
|
|
|
|
absPath, err := filepath.Abs(parentPath)
|
|
if err != nil {
|
|
log.Tracef("could not get absolut parent process path: %s", err)
|
|
return
|
|
}
|
|
|
|
root := filepath.Dir(registry.StorageDir().Path)
|
|
if !strings.HasPrefix(absPath, root) {
|
|
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
|
|
notifications.NotifyWarn(
|
|
"updates:unsupported-parent",
|
|
"Unsupported Launcher",
|
|
fmt.Sprintf(
|
|
"The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
|
|
expectedFileName,
|
|
absPath,
|
|
filepath.Join(root, expectedFileName),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
|
fileExists := false
|
|
_, err := os.Stat(fileToUpgrade)
|
|
if err == nil {
|
|
// file exists and is accessible
|
|
fileExists = true
|
|
}
|
|
|
|
if fileExists {
|
|
// get current version
|
|
var currentVersion string
|
|
cmd := exec.Command(fileToUpgrade, "version", "--short")
|
|
out, err := cmd.Output()
|
|
if err == nil {
|
|
// abort if version matches
|
|
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
|
if currentVersion == file.Version() {
|
|
log.Tracef("updates: %s is already v%s", fileToUpgrade, file.Version())
|
|
// already up to date!
|
|
return nil
|
|
}
|
|
} else {
|
|
log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err)
|
|
currentVersion = "0.0.0"
|
|
}
|
|
|
|
// test currentVersion for sanity
|
|
if !rawVersionRegex.MatchString(currentVersion) {
|
|
log.Tracef("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
|
|
}
|
|
|
|
// try removing old version
|
|
err = os.Remove(fileToUpgrade)
|
|
if err != nil {
|
|
// ensure tmp dir is here
|
|
err = registry.TmpDir().Ensure()
|
|
if err != nil {
|
|
return fmt.Errorf("could not prepare tmp directory for moving file that needs upgrade: %w", err)
|
|
}
|
|
|
|
// maybe we're on windows and it's in use, try moving
|
|
err = os.Rename(fileToUpgrade, filepath.Join(
|
|
registry.TmpDir().Path,
|
|
fmt.Sprintf(
|
|
"%s-%d%s",
|
|
filepath.Base(fileToUpgrade),
|
|
time.Now().UTC().Unix(),
|
|
upgradedSuffix,
|
|
),
|
|
))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to move file that needs upgrade: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy upgrade
|
|
err = CopyFile(file.Path(), fileToUpgrade)
|
|
if err != nil {
|
|
// try again
|
|
time.Sleep(1 * time.Second)
|
|
err = CopyFile(file.Path(), fileToUpgrade)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// check permissions
|
|
if !onWindows {
|
|
info, err := os.Stat(fileToUpgrade)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file info on %s: %w", fileToUpgrade, err)
|
|
}
|
|
if info.Mode() != 0755 {
|
|
err := os.Chmod(fileToUpgrade, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set permissions on %s: %w", fileToUpgrade, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Infof("updates: upgraded %s to v%s", fileToUpgrade, file.Version())
|
|
return nil
|
|
}
|
|
|
|
// CopyFile atomically copies a file using the update registry's tmp dir.
|
|
func CopyFile(srcPath, dstPath string) (err error) {
|
|
|
|
// check tmp dir
|
|
err = registry.TmpDir().Ensure()
|
|
if err != nil {
|
|
return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
|
|
}
|
|
|
|
// open file for writing
|
|
atomicDstFile, err := renameio.TempFile(registry.TmpDir().Path, dstPath)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create temp file for atomic copy: %w", err)
|
|
}
|
|
defer atomicDstFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway
|
|
|
|
// open source
|
|
srcFile, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
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: %w", dstPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|