Merge branch 'v2.0' into task/refactor-spn

This commit is contained in:
Natanael Rodriguez Ramos
2025-05-18 20:59:38 +01:00
47 changed files with 947 additions and 8269 deletions

View File

@@ -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)
@@ -119,8 +130,8 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
Directory: svcCfg.BinDir,
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
Ignore: []string{"databases", "intel", "config.json"},
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
Ignore: []string{"uninstall.exe"}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
IndexFile: "index.json",
Verify: svcCfg.VerifyBinaryUpdates,
AutoCheck: true, // May be changed by config during instance startup.
@@ -150,7 +161,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
Directory: svcCfg.BinDir,
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
Ignore: []string{"databases", "intel", "config.json"},
Ignore: []string{}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
IndexFile: "index.json",
Verify: svcCfg.VerifyBinaryUpdates,
@@ -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"),

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
@@ -72,7 +73,7 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
specialProfileID = profile.PortmasterProfileID
default:
// Check if this is another Portmaster component.
if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath {
if p.IsPortmasterUi(context.Background()) {
specialProfileID = profile.PortmasterAppProfileID
}
// Check if this is the system resolver.
@@ -104,3 +105,37 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
return specialProfileID
}
// IsPortmasterUi checks if the process is the Portmaster UI or its child (up to 3 parent levels).
func (p *Process) IsPortmasterUi(ctx context.Context) bool {
if module.portmasterUIPath == "" {
return false
}
// Find parent for up to two levels, if we don't match the path.
const checkLevels = 3
var previousPid int
proc := p
for i := 0; i < checkLevels; i++ {
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
break
}
realPath, err := filepath.EvalSymlinks(proc.Path)
if err == nil && realPath == module.portmasterUIPath {
return true
}
if i < checkLevels-1 { // no need to check parent if we are at the last level
previousPid = proc.Pid
proc, err = GetOrFindProcess(ctx, proc.ParentPid)
if err != nil || proc.Pid == previousPid {
break
}
}
}
return false
}

View File

@@ -3,17 +3,22 @@ package binmeta
import (
"bytes"
"fmt"
"image"
_ "image/png" // Register png support for image package
"github.com/fogleman/gg"
_ "github.com/mat/besticon/ico" // Register ico support for image package
// Import the specialized ICO decoder package
// This package seems to work better than "github.com/mat/besticon/ico" with ICO files
// extracted from Windows binaries, particularly those containing cursor-related data
ico "github.com/sergeymakinen/go-ico"
)
// ConvertICOtoPNG converts a an .ico to a .png image.
func ConvertICOtoPNG(ico []byte) (png []byte, err error) {
// Decode the ICO.
icon, _, err := image.Decode(bytes.NewReader(ico))
func ConvertICOtoPNG(icoBytes []byte) (png []byte, err error) {
// Decode ICO image.
// Note: The standard approach with `image.Decode(bytes.NewReader(icoBytes))` sometimes fails
// when processing certain ICO files (particularly those with cursor data),
// as it reads initial bytes for format detection before passing the stream to the decoder.
icon, err := ico.Decode(bytes.NewReader(icoBytes))
if err != nil {
return nil, fmt.Errorf("failed to decode ICO: %w", err)
}

View File

@@ -538,13 +538,14 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
}
// Apply new icon if found.
if newIcon != nil {
if newIcon != nil && !profile.iconExists(newIcon) {
if len(profile.Icons) == 0 {
profile.Icons = []binmeta.Icon{*newIcon}
} else {
profile.Icons = append(profile.Icons, *newIcon)
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
}
changed = true
}
}()
@@ -559,3 +560,13 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
return nil
}
// Checks if the given icon already assigned to the profile.
func (profile *Profile) iconExists(newIcon *binmeta.Icon) bool {
for _, icon := range profile.Icons {
if icon.Value == newIcon.Value && icon.Type == newIcon.Type && icon.Source == newIcon.Source {
return true
}
}
return false
}

View File

@@ -184,7 +184,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
case !rrCache.Expired():
// Return non-expired cached entry immediately.
return rrCache, nil
case useStaleCache():
case rrCache.RCode == dns.RcodeSuccess && useStaleCache():
// Return expired cache if we should use stale cache entries,
// but start an async query instead.
log.Tracer(ctx).Tracef(

View File

@@ -107,15 +107,21 @@ func (tr *TCPResolver) UseTLS() *TCPResolver {
}
func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolverConn, error) {
var existingConn *tcpResolverConn
// Minimize the time we hold the lock to avoid blocking other threads.
tr.Lock()
defer tr.Unlock()
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
existingConn = tr.resolverConn
}
tr.Unlock()
// Check if we have a resolver.
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
if existingConn != nil {
// If there is one, check if it's alive!
select {
case tr.resolverConn.heartbeat <- struct{}{}:
return tr.resolverConn, nil
case existingConn.heartbeat <- struct{}{}:
return existingConn, nil
case <-time.After(heartbeatTimeout):
log.Warningf("resolver: heartbeat for dns client %s failed", tr.resolver.Info.DescriptiveName())
case <-ctx.Done():
@@ -162,6 +168,10 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
tr.resolver.Info.DescriptiveName(),
)
// Thread-safe resolverConn creation.
tr.Lock()
defer tr.Unlock()
// Create resolver connection.
tr.resolverConnInstanceID++
resolverConn := &tcpResolverConn{

View File

@@ -50,6 +50,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.
@@ -87,6 +103,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.
@@ -404,7 +423,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
Type: notifications.ActionTypeWebhook,
Payload: notifications.ActionTypeWebhookPayload{
Method: "POST",
URL: "updates/apply",
URL: "core/restart",
},
},
},

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
@@ -24,12 +25,20 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
}
}
// Unload UI assets to be able to move files on Windows.
u.instance.UI().EnableUpgradeLock()
defer u.instance.UI().DisableUpgradeLock()
// If we are running in a UI instance, we need to unload the UI assets
if u.instance != nil {
u.instance.UI().EnableUpgradeLock()
defer u.instance.UI().DisableUpgradeLock()
}
// 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
}
@@ -207,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
}