Merge branch 'v2.0' into feature/ui-security
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -115,12 +126,12 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
binaryUpdateConfig = &updates.Config{
|
||||
Name: "binaries",
|
||||
Name: configure.DefaultBinaryIndexName,
|
||||
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.
|
||||
@@ -130,7 +141,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
Notify: true,
|
||||
}
|
||||
intelUpdateConfig = &updates.Config{
|
||||
Name: "intel",
|
||||
Name: configure.DefaultIntelIndexName,
|
||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
||||
@@ -146,11 +157,11 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
||||
|
||||
case "linux":
|
||||
binaryUpdateConfig = &updates.Config{
|
||||
Name: "binaries",
|
||||
Name: configure.DefaultBinaryIndexName,
|
||||
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,8 +171,23 @@ 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: "intel",
|
||||
Name: configure.DefaultIntelIndexName,
|
||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultBinaryIndexName = "Portmaster Binaries"
|
||||
DefaultIntelIndexName = "intel"
|
||||
|
||||
DefaultStableBinaryIndexURLs = []string{
|
||||
"https://updates.safing.io/stable.v3.json",
|
||||
}
|
||||
|
||||
@@ -438,6 +438,15 @@ func (i *Instance) BinaryUpdates() *updates.Updater {
|
||||
return i.binaryUpdates
|
||||
}
|
||||
|
||||
// GetBinaryUpdateFile returns the file path of a binary update file.
|
||||
func (i *Instance) GetBinaryUpdateFile(name string) (path string, err error) {
|
||||
file, err := i.binaryUpdates.GetFile(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return file.Path(), nil
|
||||
}
|
||||
|
||||
// IntelUpdates returns the updates module.
|
||||
func (i *Instance) IntelUpdates() *updates.Updater {
|
||||
return i.intelUpdates
|
||||
|
||||
@@ -19,7 +19,7 @@ var (
|
||||
// pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs.
|
||||
pidsByUser = make(map[int][]int)
|
||||
pidsByUserLock sync.RWMutex
|
||||
fetchPidsByUser = utils.NewCallLimiter(10 * time.Millisecond)
|
||||
fetchPidsByUser = utils.NewCallLimiter2(10 * time.Millisecond)
|
||||
)
|
||||
|
||||
// getPidsByUser returns the cached PIDs for the given UID.
|
||||
|
||||
@@ -25,7 +25,7 @@ type tcpTable struct {
|
||||
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
||||
lastUpdateAt atomic.Int64
|
||||
|
||||
fetchLimiter *utils.CallLimiter
|
||||
fetchLimiter *utils.CallLimiter2
|
||||
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
|
||||
|
||||
dualStack *tcpTable
|
||||
@@ -34,13 +34,13 @@ type tcpTable struct {
|
||||
var (
|
||||
tcp6Table = &tcpTable{
|
||||
version: 6,
|
||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
||||
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||
fetchTable: getTCP6Table,
|
||||
}
|
||||
|
||||
tcp4Table = &tcpTable{
|
||||
version: 4,
|
||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
||||
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||
fetchTable: getTCP4Table,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ type udpTable struct {
|
||||
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
||||
lastUpdateAt atomic.Int64
|
||||
|
||||
fetchLimiter *utils.CallLimiter
|
||||
fetchLimiter *utils.CallLimiter2
|
||||
fetchTable func() (binds []*socket.BindInfo, err error)
|
||||
|
||||
states map[string]map[string]*udpState
|
||||
@@ -52,14 +52,14 @@ const (
|
||||
var (
|
||||
udp6Table = &udpTable{
|
||||
version: 6,
|
||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
||||
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||
fetchTable: getUDP6Table,
|
||||
states: make(map[string]map[string]*udpState),
|
||||
}
|
||||
|
||||
udp4Table = &udpTable{
|
||||
version: 4,
|
||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
||||
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||
fetchTable: getUDP4Table,
|
||||
states: make(map[string]map[string]*udpState),
|
||||
}
|
||||
|
||||
@@ -2,35 +2,21 @@ package ui
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
func (ui *UI) registerAPIEndpoints() error {
|
||||
return api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "ui/reload",
|
||||
Write: api.PermitUser,
|
||||
ActionFunc: reloadUI,
|
||||
ActionFunc: ui.reloadUI,
|
||||
Name: "Reload UI Assets",
|
||||
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.",
|
||||
})
|
||||
}
|
||||
|
||||
func reloadUI(_ *api.Request) (msg string, err error) {
|
||||
appsLock.Lock()
|
||||
defer appsLock.Unlock()
|
||||
|
||||
func (ui *UI) reloadUI(_ *api.Request) (msg string, err error) {
|
||||
// Close all archives.
|
||||
for id, archiveFS := range apps {
|
||||
err := archiveFS.Close()
|
||||
if err != nil {
|
||||
log.Warningf("ui: failed to close archive %s: %s", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset index.
|
||||
for key := range apps {
|
||||
delete(apps, key)
|
||||
}
|
||||
ui.CloseArchives()
|
||||
|
||||
return "all ui archives successfully reloaded", nil
|
||||
}
|
||||
|
||||
@@ -1,27 +1,55 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"github.com/spkg/zipfs"
|
||||
)
|
||||
|
||||
func prep() error {
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
// UI serves the user interface files.
|
||||
type UI struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
|
||||
return registerRoutes()
|
||||
archives map[string]*zipfs.FileSystem
|
||||
archivesLock sync.RWMutex
|
||||
|
||||
upgradeLock atomic.Bool
|
||||
}
|
||||
|
||||
func start() error {
|
||||
// New returns a new UI module.
|
||||
func New(instance instance) (*UI, error) {
|
||||
m := mgr.New("UI")
|
||||
ui := &UI{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
archives: make(map[string]*zipfs.FileSystem),
|
||||
}
|
||||
|
||||
if err := ui.registerAPIEndpoints(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ui.registerRoutes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
func (ui *UI) Manager() *mgr.Manager {
|
||||
return ui.mgr
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (ui *UI) Start() error {
|
||||
// Create a dummy directory to which processes change their working directory
|
||||
// to. Currently this includes the App and the Notifier. The aim is protect
|
||||
// all other directories and increase compatibility should any process want
|
||||
@@ -30,7 +58,7 @@ func start() error {
|
||||
// may seem dangerous, but proper permission on the parent directory provide
|
||||
// (some) protection.
|
||||
// Processes must _never_ read from this directory.
|
||||
execDir := filepath.Join(module.instance.DataDir(), "exec")
|
||||
execDir := filepath.Join(ui.instance.DataDir(), "exec")
|
||||
err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional.
|
||||
if err != nil {
|
||||
log.Warningf("ui: failed to create safe exec dir: %s", err)
|
||||
@@ -45,52 +73,67 @@ func start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UI serves the user interface files.
|
||||
type UI struct {
|
||||
mgr *mgr.Manager
|
||||
|
||||
instance instance
|
||||
}
|
||||
|
||||
func (ui *UI) Manager() *mgr.Manager {
|
||||
return ui.mgr
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (ui *UI) Start() error {
|
||||
return start()
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (ui *UI) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
shimLoaded atomic.Bool
|
||||
module *UI
|
||||
)
|
||||
func (ui *UI) getArchive(name string) (archive *zipfs.FileSystem, ok bool) {
|
||||
ui.archivesLock.RLock()
|
||||
defer ui.archivesLock.RUnlock()
|
||||
|
||||
// New returns a new UI module.
|
||||
func New(instance instance) (*UI, error) {
|
||||
if !shimLoaded.CompareAndSwap(false, true) {
|
||||
return nil, errors.New("only one instance allowed")
|
||||
}
|
||||
m := mgr.New("UI")
|
||||
module = &UI{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
archive, ok = ui.archives[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (ui *UI) setArchive(name string, archive *zipfs.FileSystem) {
|
||||
ui.archivesLock.Lock()
|
||||
defer ui.archivesLock.Unlock()
|
||||
|
||||
ui.archives[name] = archive
|
||||
}
|
||||
|
||||
// CloseArchives closes all open archives.
|
||||
func (ui *UI) CloseArchives() {
|
||||
if ui == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := prep(); err != nil {
|
||||
return nil, err
|
||||
ui.archivesLock.Lock()
|
||||
defer ui.archivesLock.Unlock()
|
||||
|
||||
// Close archives.
|
||||
for _, archive := range ui.archives {
|
||||
if err := archive.Close(); err != nil {
|
||||
ui.mgr.Warn("failed to close ui archive", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return module, nil
|
||||
// Reset map.
|
||||
clear(ui.archives)
|
||||
}
|
||||
|
||||
// EnableUpgradeLock enables the upgrade lock and closes all open archives.
|
||||
func (ui *UI) EnableUpgradeLock() {
|
||||
if ui == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ui.upgradeLock.Store(true)
|
||||
ui.CloseArchives()
|
||||
}
|
||||
|
||||
// DisableUpgradeLock disables the upgrade lock.
|
||||
func (ui *UI) DisableUpgradeLock() {
|
||||
if ui == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ui.upgradeLock.Store(false)
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
DataDir() string
|
||||
API() *api.API
|
||||
BinaryUpdates() *updates.Updater
|
||||
GetBinaryUpdateFile(name string) (path string, err error)
|
||||
}
|
||||
|
||||
@@ -9,26 +9,19 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spkg/zipfs"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
var (
|
||||
apps = make(map[string]*zipfs.FileSystem)
|
||||
appsLock sync.RWMutex
|
||||
)
|
||||
|
||||
func registerRoutes() error {
|
||||
func (ui *UI) registerRoutes() error {
|
||||
// Server assets.
|
||||
api.RegisterHandler(
|
||||
"/assets/{resPath:[a-zA-Z0-9/\\._-]+}",
|
||||
&archiveServer{defaultModuleName: "assets"},
|
||||
&archiveServer{ui: ui, defaultModuleName: "assets"},
|
||||
)
|
||||
|
||||
// Add slash to plain module namespaces.
|
||||
@@ -38,7 +31,7 @@ func registerRoutes() error {
|
||||
)
|
||||
|
||||
// Serve modules.
|
||||
srv := &archiveServer{}
|
||||
srv := &archiveServer{ui: ui}
|
||||
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/", srv)
|
||||
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", srv)
|
||||
|
||||
@@ -52,6 +45,7 @@ func registerRoutes() error {
|
||||
}
|
||||
|
||||
type archiveServer struct {
|
||||
ui *UI
|
||||
defaultModuleName string
|
||||
}
|
||||
|
||||
@@ -82,39 +76,35 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
resPath = "index.html"
|
||||
}
|
||||
|
||||
appsLock.RLock()
|
||||
archiveFS, ok := apps[moduleName]
|
||||
appsLock.RUnlock()
|
||||
archiveFS, ok := bs.ui.getArchive(moduleName)
|
||||
if ok {
|
||||
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the upgrade lock is enabled.
|
||||
if bs.ui.upgradeLock.Load() {
|
||||
http.Error(w, "Resources locked, upgrade in progress.", http.StatusLocked)
|
||||
return
|
||||
}
|
||||
|
||||
// get file from update system
|
||||
zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName))
|
||||
zipFile, err := bs.ui.instance.GetBinaryUpdateFile(fmt.Sprintf("%s.zip", moduleName))
|
||||
if err != nil {
|
||||
if errors.Is(err, updates.ErrNotFound) {
|
||||
log.Tracef("ui: requested module %s does not exist", moduleName)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
} else {
|
||||
log.Tracef("ui: error loading module %s: %s", moduleName, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
log.Tracef("ui: error loading module %s: %s", moduleName, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Open archive from disk.
|
||||
archiveFS, err = zipfs.New(zipFile.Path())
|
||||
archiveFS, err = zipfs.New(zipFile)
|
||||
if err != nil {
|
||||
log.Tracef("ui: error prepping module %s: %s", moduleName, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
appsLock.Lock()
|
||||
apps[moduleName] = archiveFS
|
||||
appsLock.Unlock()
|
||||
|
||||
bs.ui.setArchive(moduleName, archiveFS)
|
||||
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,14 @@ type Index struct {
|
||||
Artifacts []*Artifact `json:"Artifacts"`
|
||||
|
||||
versionNum *semver.Version
|
||||
|
||||
// isLocallyGenerated indicates whether the index was generated from a local directory.
|
||||
//
|
||||
// When true:
|
||||
// - The `Published` field represents the generation time, not a formal release date.
|
||||
// This timestamp should be ignored when checking for online updates.
|
||||
// - Downgrades from this locally generated version to an online index should be prevented.
|
||||
isLocallyGenerated bool
|
||||
}
|
||||
|
||||
// LoadIndex loads and parses an index from the given filename.
|
||||
@@ -235,6 +243,15 @@ func (index *Index) ShouldUpgradeTo(newIndex *Index) error {
|
||||
case index.Name != newIndex.Name:
|
||||
return errors.New("new index name does not match")
|
||||
|
||||
case index.isLocallyGenerated:
|
||||
if newIndex.versionNum.GreaterThan(index.versionNum) {
|
||||
// Upgrade! (from a locally generated index to an online index)
|
||||
return nil
|
||||
} else {
|
||||
// "Do nothing".
|
||||
return ErrSameIndex
|
||||
}
|
||||
|
||||
case index.Published.After(newIndex.Published):
|
||||
return errors.New("new index is older (time)")
|
||||
|
||||
|
||||
@@ -234,10 +234,11 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error)
|
||||
|
||||
// Create base index.
|
||||
index := &Index{
|
||||
Name: cfg.Name,
|
||||
Version: cfg.Version,
|
||||
Published: time.Now(),
|
||||
versionNum: indexVersion,
|
||||
Name: cfg.Name,
|
||||
Version: cfg.Version,
|
||||
Published: time.Now(),
|
||||
versionNum: indexVersion,
|
||||
isLocallyGenerated: true,
|
||||
}
|
||||
if index.Version == "" && cfg.PrimaryArtifact != "" {
|
||||
pv, ok := artifacts[cfg.PrimaryArtifact]
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/configure"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/ui"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,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.
|
||||
@@ -86,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.
|
||||
@@ -201,6 +222,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) {
|
||||
module.corruptedInstallation = fmt.Errorf("invalid index: %w", err)
|
||||
}
|
||||
index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{
|
||||
Name: configure.DefaultBinaryIndexName,
|
||||
Version: info.VersionNumber(),
|
||||
})
|
||||
if err == nil && index.init(currentPlatform) == nil {
|
||||
@@ -402,7 +424,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",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -643,4 +665,5 @@ type instance interface {
|
||||
Restart()
|
||||
Shutdown()
|
||||
Notifications() *notifications.Notifications
|
||||
UI() *ui.UI
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -24,8 +25,20 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -73,6 +86,10 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader) error {
|
||||
if slices.Contains(u.cfg.Ignore, file.Name()) {
|
||||
continue
|
||||
}
|
||||
// ignore PurgeDirectory itself
|
||||
if strings.EqualFold(u.cfg.PurgeDirectory, filepath.Join(u.cfg.Directory, file.Name())) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, move file to purge dir.
|
||||
src := filepath.Join(u.cfg.Directory, file.Name())
|
||||
@@ -199,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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user