summaryrefslogtreecommitdiffstats
path: root/lib/puppet/provider/service/launchd.rb
blob: 9703595395b8f7dbc6f979d6a0afab6bffd0b130 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
require 'facter/util/plist'

Puppet::Type.type(:service).provide :launchd, :parent => :base do
  desc "launchd service management framework.

  This provider manages launchd jobs, the default service framework for
  Mac OS X, that has also been open sourced by Apple for possible use on
  other platforms.

  See:
  * http://developer.apple.com/macosx/launchd.html
  * http://launchd.macosforge.org/

  This provider reads plists out of the following directories:
  * /System/Library/LaunchDaemons
  * /System/Library/LaunchAgents
  * /Library/LaunchDaemons
  * /Library/LaunchAgents

  and builds up a list of services based upon each plists \"Label\" entry.

  This provider supports:
  * ensure => running/stopped,
  * enable => true/false
  * status
  * restart

  Here is how the Puppet states correspond to launchd states:
  * stopped => job unloaded
  * started => job loaded
  * enabled => 'Disable' removed from job plist file
  * disabled => 'Disable' added to job plist file

  Note that this allows you to do something launchctl can't do, which is to
  be in a state of \"stopped/enabled\ or \"running/disabled\".

  "

  commands :launchctl => "/bin/launchctl"
  commands :sw_vers => "/usr/bin/sw_vers"

  defaultfor :operatingsystem => :darwin
  confine :operatingsystem => :darwin

  has_feature :enableable

  Launchd_Paths = ["/Library/LaunchAgents",
    "/Library/LaunchDaemons",
    "/System/Library/LaunchAgents",
    "/System/Library/LaunchDaemons",]

  Launchd_Overrides = "/var/db/launchd.db/com.apple.launchd/overrides.plist"


  # returns a label => path map for either all jobs, or just a single
  # job if the label is specified
  def self.jobsearch(label=nil)
    label_to_path_map = {}
    Launchd_Paths.each do |path|
      if FileTest.exists?(path)
        Dir.entries(path).each do |f|
          next if f =~ /^\..*$/
          next if FileTest.directory?(f)
          fullpath = File.join(path, f)
          job = Plist::parse_xml(fullpath)
          if job and job.has_key?("Label")
            if job["Label"] == label
              return { label => fullpath }
            else
              label_to_path_map[job["Label"]] = fullpath
            end
          end
        end
      end
    end

    # if we didn't find the job above and we should have, error.
    raise Puppet::Error.new("Unable to find launchd plist for job: #{label}") if label
    # if returning all jobs
    label_to_path_map
  end


  def self.instances
    jobs = self.jobsearch
    jobs.keys.collect do |job|
      new(:name => job, :provider => :launchd, :path => jobs[job])
    end
  end


  def self.get_macosx_version_major
    return @macosx_version_major if defined?(@macosx_version_major)
    begin
      # Make sure we've loaded all of the facts
      Facter.loadfacts

      if Facter.value(:macosx_productversion_major)
        product_version_major = Facter.value(:macosx_productversion_major)
      else
        # TODO: remove this code chunk once we require Facter 1.5.5 or higher.
        Puppet.warning("DEPRECATION WARNING: Future versions of the launchd provider will require Facter 1.5.5 or newer.")
        product_version = Facter.value(:macosx_productversion)
        fail("Could not determine OS X version from Facter") if product_version.nil?
        product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
      end
      fail("#{product_version_major} is not supported by the launchd provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major)
      @macosx_version_major = product_version_major
      return @macosx_version_major
    rescue Puppet::ExecutionFailure => detail
      fail("Could not determine OS X version: #{detail}")
    end
  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]
    job_plist = Plist::parse_xml(job_path)
    raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}") if not job_plist
    [job_path, job_plist]
  end


  def status
    # launchctl list <jobname> exits zero if the job is loaded
    # and non-zero if it isn't. Simple way to check... but is only
    # available on OS X 10.5 unfortunately, so we grab the whole list
    # and check if our resource is included. The output formats differ
    # between 10.4 and 10.5, thus the necessity for splitting
    begin
      output = launchctl :list
      raise Puppet::Error.new("launchctl list failed to return any data.") if output.nil?
      output.split("\n").each do |j|
        return :running if j.split(/\s/).last == resource[:name]
      end
      return :stopped
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new("Unable to determine status of #{resource[:name]}")
    end
  end


  # start the service. To get to a state of running/enabled, we need to
  # conditionally enable at load, then disable by modifying the plist file
  # directly.
  def start
    job_path, job_plist = plist_from_label(resource[:name])
    did_enable_job = false
    cmds = []
    cmds << :launchctl << :load
    if self.enabled? == :false  # launchctl won't load disabled jobs
      cmds << "-w"
      did_enable_job = true
    end
    cmds << job_path
    begin
      execute(cmds)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new("Unable to start service: #{resource[:name]} at path: #{job_path}")
    end
    # As load -w clears the Disabled flag, we need to add it in after
    self.disable if did_enable_job and resource[:enable] == :false
  end


  def stop
    job_path, job_plist = plist_from_label(resource[:name])
    did_disable_job = false
    cmds = []
    cmds << :launchctl << :unload
    if self.enabled? == :true # keepalive jobs can't be stopped without disabling
      cmds << "-w"
      did_disable_job = true
    end
    cmds << job_path
    begin
      execute(cmds)
    rescue Puppet::ExecutionFailure
      raise Puppet::Error.new("Unable to stop service: #{resource[:name]} at path: #{job_path}")
    end
    # As unload -w sets the Disabled flag, we need to add it in after
    self.enable if did_disable_job and resource[:enable] == :true
  end


  # launchd jobs are enabled by default. They are only disabled if the key
  # "Disabled" is set to true, but it can also be set to false to enable it.
  # In 10.6, the Disabled key in the job plist is consulted, but only if there
  # is no entry in the global overrides plist.
  # We need to draw a distinction between undefined, true and false for both
  # locations where the Disabled flag can be defined.
  def enabled?
    job_plist_disabled = nil
    overrides_disabled = nil

    job_path, job_plist = plist_from_label(resource[:name])
    job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled")

    if self.class.get_macosx_version_major == "10.6":
      overrides = Plist::parse_xml(Launchd_Overrides)

      unless overrides.nil?
        if overrides.has_key?(resource[:name])
          overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled")
        end
      end
    end

    if overrides_disabled.nil?
      if job_plist_disabled.nil? or job_plist_disabled == false
        return :true
      end
    elsif overrides_disabled == false
      return :true
    end
    :false
  end


  # enable and disable are a bit hacky. We write out the plist with the appropriate value
  # rather than dealing with launchctl as it is unable to change the Disabled flag
  # without actually loading/unloading the job.
  # In 10.6 we need to write out a disabled key to the global overrides plist, in earlier
  # versions this is stored in the job plist itself.
  def enable
    if self.class.get_macosx_version_major == "10.6"
      overrides = Plist::parse_xml(Launchd_Overrides)
      overrides[resource[:name]] = { "Disabled" => false }
      Plist::Emit.save_plist(overrides, Launchd_Overrides)
    else
      job_path, job_plist = plist_from_label(resource[:name])
      if self.enabled? == :false
        job_plist.delete("Disabled")
        Plist::Emit.save_plist(job_plist, job_path)
      end
    end
  end


  def disable
    if self.class.get_macosx_version_major == "10.6"
      overrides = Plist::parse_xml(Launchd_Overrides)
      overrides[resource[:name]] = { "Disabled" => true }
      Plist::Emit.save_plist(overrides, Launchd_Overrides)
    else
      job_path, job_plist = plist_from_label(resource[:name])
      job_plist["Disabled"] = true
      Plist::Emit.save_plist(job_plist, job_path)
    end
  end


end