puppetlabs/puppet · error · Puppet::Error

Unable to parse launchd plist at path: #{job_path}

Error message

Unable to parse launchd plist at path: #{job_path}

What it means

After jobsearch resolves a label to a plist path, plist_from_label guards with FileTest.file?(job_path) and raises when the path is not a regular file. This catches a stale label-to-path mapping: the map said the plist lives there, but on disk the file was deleted, replaced by a directory, or is a dangling symlink between the map build and the read.

Source

Thrown at lib/puppet/provider/service/launchd.rb:242

  # Clean out the @property_hash variable containing the cached list of services
  def flush
    @property_hash.clear
  end

  def exists?
    Puppet.debug("Puppet::Provider::Launchd:Ensure for #{@property_hash[:name]}: #{@property_hash[:ensure]}")
    @property_hash[:ensure] != :absent
  end

  # finds the path for a given label and returns the path and parsed plist
  # as an array of [path, plist]. Note plist is really a Hash here.
  def plist_from_label(label)
    job = self.class.jobsearch(label)
    job_path = job[label]
    if FileTest.file?(job_path)
      job_plist = self.class.read_plist(job_path)
    else
      raise Puppet::Error, "Unable to parse launchd plist at path: #{job_path}"
    end
    [job_path, job_plist]
  end

  # when a service includes hasstatus=>false, we override the launchctl
  # status mechanism and fall back to the base provider status method.
  def status
    if @resource && ((@resource[:hasstatus] == :false) || (@resource[:status]))
      super
    elsif @property_hash[:status].nil?
      # property_hash was flushed so the service changed status
      service_name = @resource[:name]
      # Updating services with new statuses
      job_list = self.class.job_list
      # if job is present in job_list, return its status
      if job_list.key?(service_name)
        job_list[service_name]
      # if job is no longer present in job_list, it was stopped

View on GitHub (pinned to e227c27540)

Solutions

  1. Check the path on disk: test -f on the job_path shown/derivable from the error
  2. Remove or repair dangling entries, then re-run so make_label_to_path_map rebuilds without them
  3. Order resources so a managed service is absent/disabled before the package removes its plist
  4. Ensure only one tool owns LaunchDaemons to avoid mid-run races
Defensive patterns

Strategy: validation

Validate before calling

# Ruby, before relying on a resolved job path
raise 'job plist missing' unless FileTest.file?(job_path)

Prevention

When it happens

Trigger: The label map was built (or refreshed once) and the plist was removed afterwards — e.g. a package resource earlier in the same run uninstalled the daemon; or jobsearch returned a path where a non-file now sits. The raise happens before self.class.read_plist is attempted.

Common situations: Uninstall/reinstall flows where a service resource still references a job whose plist a package just removed; broken symlinks in /Library/LaunchDaemons left by hand-rolled uninstall scripts; concurrent management tools deleting plists mid-run.

Understand the failure class

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/2676f53e57cf3274. Report an issue: GitHub.