pranshuparmar/witr · error

failed to convert plist: %w

Error message

failed to convert plist: %w

What it means

ParsePlist shells out to `plutil -convert xml1 -o -` to normalize (possibly binary) launchd plists into XML before parsing. Any failure of that external command — missing plutil, unreadable file, corrupt plist — is wrapped as 'failed to convert plist: %w'. The wrapped error contains plutil's own diagnostic.

Source

Thrown at internal/launchd/plist.go:156

		if strings.HasPrefix(path, "~") {
			path = filepath.Join(homeDir, path[1:])
		}

		plistPath := filepath.Join(path, label+".plist")
		if _, err := os.Stat(plistPath); err == nil {
			return plistPath
		}
	}

	return ""
}

// ParsePlist reads and parses a launchd plist file
func ParsePlist(path string) (*LaunchdInfo, error) {
	// Use plutil to convert to XML (handles binary plists)
	out, err := exec.Command("plutil", "-convert", "xml1", "-o", "-", path).Output()
	if err != nil {
		return nil, fmt.Errorf("failed to convert plist: %w", err)
	}

	info := &LaunchdInfo{
		PlistPath: path,
	}

	// Parse the XML plist
	if err := parsePlistXML(out, info); err != nil {
		return nil, err
	}

	return info, nil
}

// parsePlistXML parses XML plist data into LaunchdInfo
func parsePlistXML(data []byte, info *LaunchdInfo) error {
	decoder := xml.NewDecoder(bytes.NewReader(data))

View on GitHub (pinned to dc4fa1da82)

Solutions

  1. Verify the plist path exists and is readable (`ls -l <path>`); the wrapped error usually says 'no such file' vs 'permission denied'.
  2. Re-run with sudo if the plist is in a protected directory (/System/Library/LaunchDaemons).
  3. Validate the file manually with `plutil -lint <path>` to see the specific plist defect.
  4. Confirm you're on macOS with plutil in PATH.
  5. Locate the correct plist via `launchctl print <domain>/<label>` if the recorded path is stale.

Example fix

// before
path := "/System/Library/LaunchDaemons/com.example.daemon.plist" // permission denied
info, err := launchd.ParsePlist(path)
// after
if _, err := os.Stat(path); os.IsNotExist(err) {
    path = filepath.Join(os.Getenv("HOME"), "Library/LaunchAgents/com.example.daemon.plist")
}
info, err := launchd.ParsePlist(path)
Defensive patterns

Strategy: validation

Validate before calling

func plistReadable(path string) error {
    info, err := os.Stat(path)
    if err != nil { return fmt.Errorf("plist missing: %w", err) }
    if info.IsDir() { return fmt.Errorf("%s is a directory", path) }
    f, err := os.Open(path)
    if err != nil { return fmt.Errorf("plist unreadable: %w", err)
    }
    f.Close()
    return nil
}
if err := plistReadable(path); err != nil { return err }

Try / catch

info, err := launchd.ParsePlist(path)
if err != nil {
    var ee *exec.ExitError
    if errors.As(err, &ee) {
        return fmt.Errorf("plutil rejected %s: %s", path, strings.TrimSpace(string(ee.Stderr)))
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetLaunchdInfo/ParsePlist when plutil exits non-zero: plist path does not exist or is unreadable (permissions), the file is not a valid plist, or plutil is absent (non-macOS or stripped environment).

Common situations: Stale plist paths from LaunchAgents/LaunchDaemons referencing deleted files; inspecting a service label whose plist lives in a root-only directory; sandboxed/CI hosts without plutil; third-party plists with XML syntax errors.

Related errors


AI-assisted analysis of pranshuparmar/witr@dc4fa1da82 (2026-09-01). Data as JSON: /api/errors/6c781347100ad64b. Report an issue: GitHub.