mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:34:14 -04:00
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
294 lines
6.7 KiB
Go
294 lines
6.7 KiB
Go
//go:build windows || darwin
|
|
|
|
package installer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
goversion "github.com/hashicorp/go-version"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/updater/downloader"
|
|
"github.com/netbirdio/netbird/client/internal/updater/reposign"
|
|
)
|
|
|
|
type Installer struct {
|
|
tempDir string
|
|
}
|
|
|
|
// New used by the service
|
|
func New() *Installer {
|
|
return &Installer{
|
|
tempDir: defaultTempDir,
|
|
}
|
|
}
|
|
|
|
// NewWithDir used by the updater process, get the tempDir from the service via cmd line
|
|
func NewWithDir(tempDir string) *Installer {
|
|
return &Installer{
|
|
tempDir: tempDir,
|
|
}
|
|
}
|
|
|
|
// RunInstallation starts the updater process to run the installation
|
|
// This will run by the original service process
|
|
func (u *Installer) RunInstallation(ctx context.Context, targetVersion string) (err error) {
|
|
resultHandler := NewResultHandler(u.tempDir)
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
if writeErr := resultHandler.WriteErr(err); writeErr != nil {
|
|
log.Errorf("failed to write error result: %v", writeErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if err := validateTargetVersion(targetVersion); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := u.mkTempDir(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var installerFile string
|
|
// Download files only when not using any third-party store
|
|
if installerType := TypeOfInstaller(ctx); installerType.Downloadable() {
|
|
log.Infof("download installer")
|
|
var err error
|
|
installerFile, err = u.downloadInstaller(ctx, installerType, targetVersion)
|
|
if err != nil {
|
|
log.Errorf("failed to download installer: %v", err)
|
|
return err
|
|
}
|
|
|
|
artifactVerify, err := reposign.NewArtifactVerify(DefaultSigningKeysBaseURL)
|
|
if err != nil {
|
|
log.Errorf("failed to create artifact verify: %v", err)
|
|
return err
|
|
}
|
|
|
|
if err := artifactVerify.Verify(ctx, targetVersion, installerFile); err != nil {
|
|
log.Errorf("artifact verification error: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Infof("running installer")
|
|
updaterPath, err := u.copyUpdater()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// the directory where the service has been installed
|
|
workspace, err := getServiceDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args := []string{
|
|
"--temp-dir", u.tempDir,
|
|
"--service-dir", workspace,
|
|
}
|
|
|
|
if isDryRunEnabled() {
|
|
args = append(args, "--dry-run=true")
|
|
}
|
|
|
|
if installerFile != "" {
|
|
args = append(args, "--installer-file", installerFile)
|
|
}
|
|
|
|
updateCmd := exec.Command(updaterPath, args...)
|
|
log.Infof("starting updater process: %s", updateCmd.String())
|
|
|
|
// Configure the updater to run in a separate session/process group
|
|
// so it survives the parent daemon being stopped
|
|
setUpdaterProcAttr(updateCmd)
|
|
|
|
// Start the updater process asynchronously
|
|
if err := updateCmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
pid := updateCmd.Process.Pid
|
|
log.Infof("updater started with PID %d", pid)
|
|
|
|
// Release the process so the OS can fully detach it
|
|
if err := updateCmd.Process.Release(); err != nil {
|
|
log.Warnf("failed to release updater process: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanUpInstallerFiles
|
|
// - the installer file (pkg, exe, msi)
|
|
// - the selfcopy updater.exe
|
|
func (u *Installer) CleanUpInstallerFiles() error {
|
|
// Check if tempDir exists
|
|
info, err := os.Stat(u.tempDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
|
|
if err := os.Remove(filepath.Join(u.tempDir, updaterBinary)); err != nil && !os.IsNotExist(err) {
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to remove updater binary: %w", err))
|
|
}
|
|
|
|
entries, err := os.ReadDir(u.tempDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
for _, ext := range binaryExtensions {
|
|
if strings.HasSuffix(strings.ToLower(name), strings.ToLower(ext)) {
|
|
if err := os.Remove(filepath.Join(u.tempDir, name)); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("failed to remove %s: %w", name, err))
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return merr.ErrorOrNil()
|
|
}
|
|
|
|
func (u *Installer) downloadInstaller(ctx context.Context, installerType Type, targetVersion string) (string, error) {
|
|
fileURL := urlWithVersionArch(installerType, targetVersion)
|
|
|
|
// Clean up temp directory on error
|
|
var success bool
|
|
defer func() {
|
|
if !success {
|
|
if err := os.RemoveAll(u.tempDir); err != nil {
|
|
log.Errorf("error cleaning up temporary directory: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
fileName := path.Base(fileURL)
|
|
if fileName == "." || fileName == "/" || fileName == "" {
|
|
return "", fmt.Errorf("invalid file URL: %s", fileURL)
|
|
}
|
|
|
|
outputFilePath := filepath.Join(u.tempDir, fileName)
|
|
if err := downloader.DownloadToFile(ctx, downloader.DefaultRetryDelay, fileURL, outputFilePath); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
success = true
|
|
return outputFilePath, nil
|
|
}
|
|
|
|
func (u *Installer) TempDir() string {
|
|
return u.tempDir
|
|
}
|
|
|
|
func (u *Installer) mkTempDir() error {
|
|
if err := os.MkdirAll(u.tempDir, 0o755); err != nil {
|
|
log.Debugf("failed to create tempdir: %s", u.tempDir)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *Installer) copyUpdater() (string, error) {
|
|
src, err := getServiceBinary()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get updater binary: %w", err)
|
|
}
|
|
|
|
dst := filepath.Join(u.tempDir, updaterBinary)
|
|
if err := copyFile(src, dst); err != nil {
|
|
return "", fmt.Errorf("failed to copy updater binary: %w", err)
|
|
}
|
|
|
|
if err := os.Chmod(dst, 0o755); err != nil {
|
|
return "", fmt.Errorf("failed to set permissions: %w", err)
|
|
}
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
func validateTargetVersion(targetVersion string) error {
|
|
if targetVersion == "" {
|
|
return fmt.Errorf("target version cannot be empty")
|
|
}
|
|
|
|
_, err := goversion.NewVersion(targetVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid target version %q: %w", targetVersion, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
log.Infof("copying %s to %s", src, dst)
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return fmt.Errorf("open source: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := in.Close(); err != nil {
|
|
log.Warnf("failed to close source file: %v", err)
|
|
}
|
|
}()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return fmt.Errorf("create destination: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := out.Close(); err != nil {
|
|
log.Warnf("failed to close destination file: %v", err)
|
|
}
|
|
}()
|
|
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
return fmt.Errorf("copy: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getServiceDir() (string, error) {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Dir(exePath), nil
|
|
}
|
|
|
|
func getServiceBinary() (string, error) {
|
|
return os.Executable()
|
|
}
|
|
|
|
func isDryRunEnabled() bool {
|
|
return strings.EqualFold(strings.TrimSpace(os.Getenv("NB_AUTO_UPDATE_DRY_RUN")), "true")
|
|
}
|