summaryrefslogtreecommitdiffstats
path: root/lib/puppet/provider/service/launchd.rb
blob: c65e1cc1ab8e9b081a5134c6521e2bb31a549ac3 (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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
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.
        if label
            raise Puppet::Error.new("Unable to find launchd plist for job: #{label}")
        end
        # 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
        if defined?(@macosx_version_major)
            return @macosx_version_major
        end
        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)
                if product_version.nil?
                    fail("Could not determine OS X version from Facter")
                end
                product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
            end
            if %w{10.0 10.1 10.2 10.3}.include?(product_version_major)
                fail("%s is not supported by the launchd provider" % product_version_major)
            end
            @macosx_version_major = product_version_major
            return @macosx_version_major
        rescue Puppet::ExecutionFailure => detail
            fail("Could not determine OS X version: %s" % 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)
        if not job_plist
            raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}")
        end
        [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
            if output.nil?
                raise Puppet::Error.new("launchctl list failed to return any data.")
            end
            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: %s at path: %s" % [resource[:name], job_path])
        end
        # As load -w clears the Disabled flag, we need to add it in after
        if did_enable_job and resource[:enable] == :false
            self.disable
        end
    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: %s at path: %s" % [resource[:name], job_path])
        end
        # As unload -w sets the Disabled flag, we need to add it in after
        if did_disable_job and resource[:enable] == :true
            self.enable
        end
    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])
        if job_plist.has_key?("Disabled")
            job_plist_disabled = job_plist["Disabled"]
        end

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

            unless overrides.nil?
                if overrides.has_key?(resource[:name])
                    if overrides[resource[:name]].has_key?("Disabled")
                        overrides_disabled = overrides[resource[:name]]["Disabled"]
                    end
                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
        return :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