Files
Allen Houchins 698c99dd00 Add Adobe Acrobat Pro as a Windows FMA (#43829)
Add Winget support for Adobe Acrobat Pro: new input JSON,
install/uninstall PowerShell scripts, and Windows output manifest (with
script refs and installer metadata). Rename Homebrew input and apps
listing to "Adobe Acrobat Pro" and add a Windows entry to apps.json.
Improve winget ingester to try version directories in descending order,
skip grouping dirs that don't contain expected manifests, fetch and
unmarshal installer and locale manifests with better logging and error
handling, and return a clear error when no valid version manifest is
found.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for Adobe Acrobat Pro on Windows with automated install
and uninstall capabilities.

* **Bug Fixes**
* Improved version handling during app installation to try multiple
candidate versions if needed.
  * Enhanced error messaging for app uninstall validation.

* **Updates**
* Standardized Adobe Acrobat Pro product naming across platforms for
consistency.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/43829?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-01 09:54:34 -05:00

146 lines
4.3 KiB
Go

//go:build darwin || windows
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/fleet"
)
type AppCommander struct {
Name string
Slug string
UniqueIdentifier string
Version string
AppPath string
UninstallScript string
InstallScript string
cfg *Config
appLogger *slog.Logger
}
func (ac *AppCommander) isFrozen() (bool, error) {
inputPath := ac.cfg.inputsPath
parts := strings.Split(ac.Slug, "/")
if len(parts) != 2 {
return false, fmt.Errorf("invalid slug format: %s, expected <name>/<platform>", ac.Slug)
}
inputPath = filepath.Join(inputPath, parts[0]+".json")
fileBytes, err := os.ReadFile(inputPath)
if err != nil {
return false, fmt.Errorf("reading app input file: %w", err)
}
var input struct {
Frozen bool `json:"frozen"`
}
if err := json.Unmarshal(fileBytes, &input); err != nil {
return false, fmt.Errorf("unmarshal app input file: %w", err)
}
return input.Frozen, nil
}
func (ac *AppCommander) extractAppVersion(installerTFR *fleet.TempFileReader) error {
if ac.Version == "latest" {
meta, err := file.ExtractInstallerMetadata(installerTFR)
if err != nil {
return err
}
ac.Version = meta.Version
err = installerTFR.Rewind()
if err != nil {
return err
}
}
return nil
}
func (ac *AppCommander) uninstallPreInstalled(ctx context.Context) {
ac.appLogger.InfoContext(ctx, "App is marked as pre-installed, attempting to run uninstall script...")
_, _, listError := ac.expectToChangeFileSystem(ctx,
func() error {
uninstalled := ac.uninstallApp(ctx)
if !uninstalled {
ac.appLogger.ErrorContext(ctx, "Failed to uninstall pre-installed app")
}
return nil
},
)
if listError != nil {
ac.appLogger.ErrorContext(ctx, fmt.Sprintf("Error listing %s directory: %v", ac.cfg.installationSearchDirectory, listError))
}
}
func (ac *AppCommander) uninstallApp(ctx context.Context) bool {
ac.appLogger.InfoContext(ctx, "Executing uninstall script for app...")
output, err := executeScript(ac.cfg, ac.UninstallScript)
if err != nil {
ac.appLogger.ErrorContext(ctx, fmt.Sprintf("Error uninstalling app: %v", err))
ac.appLogger.ErrorContext(ctx, fmt.Sprintf("Output: %s", output))
return false
}
ac.appLogger.DebugContext(ctx, fmt.Sprintf("Output: %s", output))
existance, err := appExists(ctx, ac.appLogger, ac.Name, ac.UniqueIdentifier, ac.Version, ac.AppPath)
if err != nil {
ac.appLogger.ErrorContext(ctx, fmt.Sprintf("Error checking if app exists after uninstall: %v", err))
return false
}
if existance {
ac.appLogger.ErrorContext(ctx, fmt.Sprintf("App still present after uninstall (expected no match for version '%s' in programs)", ac.Version))
return false
}
return true
}
func (ac *AppCommander) expectToChangeFileSystem(ctx context.Context, changer func() error) (string, error, error) {
var preListError, postListError, listError error
appListPre, err := listDirectoryContents(ac.cfg.installationSearchDirectory)
if err != nil {
preListError = fmt.Errorf("Error listing %s directory: %v", ac.cfg.installationSearchDirectory, err)
}
changerError := changer()
appListPost, err := listDirectoryContents(ac.cfg.installationSearchDirectory)
if err != nil {
postListError = fmt.Errorf("Error listing %s directory: %v", ac.cfg.installationSearchDirectory, err)
}
appPath, changed := detectApplicationChange(ac.cfg.installationSearchDirectory, appListPre, appListPost)
if appPath == "" {
ac.appLogger.WarnContext(ctx, fmt.Sprintf("no changes detected in %s directory after running application script.", ac.cfg.installationSearchDirectory))
} else {
if changed {
ac.appLogger.InfoContext(ctx, fmt.Sprintf("New application detected at: %s", appPath))
} else {
ac.appLogger.InfoContext(ctx, fmt.Sprintf("Application removal detected at: %s", appPath))
}
}
switch {
case preListError != nil && postListError != nil:
listError = fmt.Errorf("app pre list: %v, app post list: %v", preListError, postListError)
case preListError != nil:
listError = fmt.Errorf("app pre list: %v", preListError)
case postListError != nil:
listError = fmt.Errorf("app post list: %v", postListError)
}
return appPath, changerError, listError
}