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 stoppedView on GitHub (pinned to e227c27540)
Solutions
- Check the path on disk: test -f on the job_path shown/derivable from the error
- Remove or repair dangling entries, then re-run so make_label_to_path_map rebuilds without them
- Order resources so a managed service is absent/disabled before the package removes its plist
- 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
- Make uninstall flows declare service ensure => absent before the package removes the plist
- Avoid dangling LaunchDaemons symlinks; clean them in the same run that removes the target
- Give launchd management a single owner (Puppet or the vendor tool) to prevent mid-run plist races
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unable to find launchd plist for job: #{label}
- Unable to read overrides plist, too many attempts
- puppet.plans/invalid-name
- launchctl list failed to return any data.
- Unknown plist format #{format}
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/2676f53e57cf3274.
Report an issue: GitHub.