diff --git a/service/config.go b/service/config.go index 4a985431..28448888 100644 --- a/service/config.go +++ b/service/config.go @@ -92,7 +92,8 @@ func (sc *ServiceConfig) Init() error { return nil } -func getCurrentBinaryFolder() (string, error) { +// returns the absolute path of the currently running executable +func getCurrentBinaryPath() (string, error) { // Get the path of the currently running executable exePath, err := os.Executable() if err != nil { @@ -105,6 +106,16 @@ func getCurrentBinaryFolder() (string, error) { return "", fmt.Errorf("failed to get absolute path: %w", err) } + return absPath, nil +} + +func getCurrentBinaryFolder() (string, error) { + // Get the absolute path of the currently running executable + absPath, err := getCurrentBinaryPath() + if err != nil { + return "", err + } + // Get the directory of the executable installDir := filepath.Dir(absPath) @@ -160,6 +171,21 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo NeedsRestart: true, Notify: true, } + if binPath, err := getCurrentBinaryPath(); err == nil { + binaryUpdateConfig.PostUpgradeCommands = []updates.UpdateCommandConfig{ + // Restore SELinux context for the new core binary after upgrade + // (`restorecon /usr/lib/portmaster/portmaster-core`) + { + Command: "restorecon", + Args: []string{binPath}, + TriggerArtifactFName: binPath, + FailOnError: false, // Ignore error: 'restorecon' may not be available on a non-SELinux systems. + }, + } + } else { + return nil, nil, fmt.Errorf("failed to get current binary path: %w", err) + } + intelUpdateConfig = &updates.Config{ Name: configure.DefaultIntelIndexName, Directory: filepath.Join(svcCfg.DataDir, "intel"), diff --git a/service/updates/module.go b/service/updates/module.go index b5f9b145..8a1b61fc 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -51,6 +51,22 @@ var ( ErrActionRequired = errors.New("action required") ) +// UpdateCommandConfig defines the configuration for a shell command +// that is executed when an update is applied +type UpdateCommandConfig struct { + // Shell command to execute + Command string + // Arguments to pass to the command + Args []string + // Execute triggers: if not empty, the command will be executed only if specified file was updated + // if empty, the command will be executed always + TriggerArtifactFName string + // FailOnError defines whether the upgrade should fail if the command fails + // true - upgrade will fail if the command fails + // false - upgrade will continue even if the command fails + FailOnError bool +} + // Config holds the configuration for the updates module. type Config struct { // Name of the updater. @@ -88,6 +104,9 @@ type Config struct { // Notify defines whether the user shall be informed about events via notifications. // If enabled, disables automatic restart after upgrade. Notify bool + + // list of shell commands needed to run after the upgrade (if any) + PostUpgradeCommands []UpdateCommandConfig } // Check looks for obvious configuration errors. diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index d0c5331f..89da6c8f 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "slices" "strings" @@ -32,6 +33,12 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error { // Execute the upgrade. upgradeError := u.upgradeMoveFiles(downloader) + if upgradeError == nil { + // Files upgraded successfully. + // Applying post-upgrade tasks, if any. + upgradeError = u.applyPostUpgradeCommands() + } + if upgradeError == nil { return nil } @@ -209,3 +216,41 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error { return nil } + +func (u *Updater) applyPostUpgradeCommands() error { + // At this point, we assume that the upgrade was successful and all files are in place. + // We need to execute the post-upgrade commands, if any. + + if len(u.cfg.PostUpgradeCommands) == 0 { + return nil + } + + // collect full paths to files that were upgraded, required to check the trigger. + upgradedFiles := make(map[string]struct{}) + for _, artifact := range u.index.Artifacts { + upgradedFiles[filepath.Join(u.cfg.Directory, artifact.Filename)] = struct{}{} + } + + // Execute post-upgrade commands. + for _, puCmd := range u.cfg.PostUpgradeCommands { + + // Check trigger to ensure that we need to run this command. + if len(puCmd.TriggerArtifactFName) > 0 { + if _, ok := upgradedFiles[puCmd.TriggerArtifactFName]; !ok { + continue + } + } + + log.Debugf("updates/%s: executing post-upgrade command: '%s %s'", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " ")) + output, err := exec.Command(puCmd.Command, puCmd.Args...).CombinedOutput() + if err != nil { + if puCmd.FailOnError { + return fmt.Errorf("post-upgrade command '%s %s' failed: %w, output: %s", puCmd.Command, strings.Join(puCmd.Args, " "), err, string(output)) + } + + log.Warningf("updates/%s: post-upgrade command '%s %s' failed, but ignored. Error: %s", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "), err) + } + } + + return nil +}