//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/updatemanager/downloader" "github.com/netbirdio/netbird/client/internal/updatemanager/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") }