# This is our main way of managing processes right now.
#
# a service is distinct from a process in that services
# can only be managed through the interface of an init script
# which is why they have a search path for initscripts and such

module Puppet

    newtype(:service) do
        @doc = "Manage running services.  Service support unfortunately varies
            widely by platform -- some platforms have very little if any
            concept of a running service, and some have a very codified and
            powerful concept.  Puppet's service support will generally be able
            to make up for any inherent shortcomings (e.g., if there is no
            'status' command, then Puppet will look in the process table for a
            command matching the service name), but the more information you
            can provide the better behaviour you will get.  Or, you can just
            use a platform that has very good service support."
        
        attr_reader :stat

#        newstate(:enabled) do
#            desc "Whether a service should be enabled to start at boot.
#                **true**/*false*/*runlevel*"
#
#            def retrieve
#                unless @parent.respond_to?(:enabled?)
#                    raise Puppet::Error, "Service %s does not support enabling"
#                end
#                @is = @parent.enabled?
#            end
#
#            munge do |should|
#                @runlevel = nil
#                case should
#                when true: return :enabled
#                when false: return :disabled
#                when /^\d+$/:
#                    @runlevel = should
#                    return :enabled
#                else
#                    raise Puppet::Error, "Invalid 'enabled' value %s" % should
#                end
#            end
#
#            def sync
#                case self.should
#                when :enabled
#                    unless @parent.respond_to?(:enable)
#                        raise Puppet::Error, "Service %s does not support enabling"
#                    end
#                    @parent.enable(@runlevel)
#                    return :service_enabled
#                when :disabled
#                    unless @parent.respond_to?(:disable)
#                        raise Puppet::Error,
#                            "Service %s does not support disabling"
#                    end
#                    @parent.disable
#                    return :service_disabled
#                end
#            end
#        end

        # Handle whether the service should actually be running right now.
        newstate(:running) do
            desc "Whether a service should be running.  **true**/*false*"

            munge do |should|
                case should
                when false,0,"0", "stopped", :stopped:
                    should = :stopped
                when true,1,"1", :running, "running":
                    should = :running
                else
                    self.warning "%s: interpreting '%s' as false" %
                        [self.class,should.inspect]
                    should = 0
                end
                return should
            end

            def retrieve
                self.is = @parent.status
                self.debug "Running value is '%s'" % self.is
            end

            def sync
                event = nil
                case self.should
                when :running
                    @parent.start
                    self.info "started"
                    return :service_started
                when :stopped
                    self.info "stopped"
                    @parent.stop
                    return :service_stopped
                else
                    self.debug "Not running '%s' and shouldn't be running" %
                        self
                end
            end
        end

        newparam(:type) do
            desc "The service type.  For most platforms, it does not make
                sense to change set this parameter, as the default is based on
                the builtin service facilities.  The service types available are:
                
                * ``base``: You must specify everything.
                * ``init``: Assumes ``start`` and ``stop`` commands exist, but you
                  must specify everything else.
                * ``debian``: Debian's own specific version of ``init``.
                * ``smf``: Solaris 10's new Service Management Facility.
                "

            defaultto { @parent.class.defaulttype }

            # Make sure we've got the actual module, not just a string
            # representing the module.
            munge do |type|
                if type.is_a?(String)
                    type = @parent.class.svctype(type.intern)
                end
                Puppet.debug "Service type is %s" % type.name
                @parent.extend(type)

                return type
            end
        end
        newparam(:binary) do
            desc "The path to the daemon.  This is only used for
                systems that do not support init scripts.  This binary will be
                used to start the service if no ``start`` parameter is
                provided."
        end
        newparam(:hasstatus) do
            desc "Declare the the service's init script has a
                functional status command.  Based on testing, it was found
                that a large number of init scripts on different platforms do
                not support any kind of status command; thus, you must specify
                manually whether the service you are running has such a
                command (or you can specify a specific command using the
                ``status`` parameter).
                
                If you do not specify anything, then the service name will be
                looked for in the process table."
        end
        newparam(:name) do
            desc "The name of the service to run.  This name
                is used to find the service in whatever service subsystem it
                is in."
            isnamevar
        end
        newparam(:path) do
            desc "The search path for finding init scripts."

            munge do |value|
                paths = []
                if value.is_a?(Array)
                    paths += value.flatten.collect { |p|
                        p.split(":")
                    }.flatten
                else
                    paths = value.split(":")
                end

                paths.each do |path|
                    if FileTest.directory?(path)
                        next
                    end
                    unless FileTest.directory?(path)
                        @parent.info("Search path %s is not a directory" % [path])
                    end
                    unless FileTest.exists?(path)
                        @parent.info("Search path %s does not exist" % [path])
                    end
                    paths.delete(path)
                end

                paths
            end
        end
        newparam(:pattern) do
            desc "The pattern to search for in the process table.
                This is used for stopping services on platforms that do not
                support init scripts, and is also used for determining service
                status on those service whose init scripts do not include a status
                command.
                
                If this is left unspecified and is needed to check the status
                of a service, then the service name will be used instead.
                
                The pattern can be a simple string or any legal Ruby pattern."
            defaultto { @parent.name }
        end
        newparam(:restart) do
            desc "Specify a *restart* command manually.  If left
                unspecified, the service will be stopped and then started."
        end
        newparam(:start) do
            desc "Specify a *start* command manually.  Most service subsystems
                support a ``start`` command, so this will not need to be
                specified."
        end
        newparam(:status) do
            desc "Specify a *status* command manually.  If left
                unspecified, the status method will be determined
                automatically, usually by looking for the service int he
                process table."
        end

        newparam(:stop) do
            desc "Specify a *stop* command manually."
        end

        # Create new subtypes of service management.
        def self.newsvctype(name, parent = nil, &block)
            if parent
                parent = self.svctype(parent)
            end
            svcname = name
            mod = Module.new
            const_set("SvcType" + name.to_s.capitalize,mod)

            # Add our parent, if it exists
            if parent
                mod.send(:include, parent)
            end

            # And now define the support methods
            code = %{
                def self.name
                    "#{svcname}"
                end

                def self.inspect
                    "SvcType(#{svcname})"
                end

                def self.to_s
                    "SvcType(#{svcname})"
                end

                def svctype
                    "#{svcname}"
                end
            }

            mod.module_eval(code)

            mod.module_eval(&block)

            @modules ||= Hash.new do |hash, key|
                if key.is_a?(String)
                    key = key.intern
                end

                if hash.include?(key)
                    hash[key]
                else
                    nil
                end
            end
            @modules[name] = mod
        end

        # Retrieve a service type.
        def self.svctype(name)
            @modules[name]
        end

        # Retrieve the default type for the current platform.
        def self.defaulttype
            unless defined? @defsvctype
                @defsvctype = nil
                os = Facter["operatingsystem"].value
                case os
                when "Debian":
                    @defsvctype = self.svctype(:debian)
                when "Solaris":
                    release = Facter["operatingsystemrelease"].value
                    if release.sub(/5\./,'').to_i < 10
                        @defsvctype = self.svctype(:init)
                    else
                        @defsvctype = self.svctype(:smf)
                    end
                else
                    if Facter["kernel"] == "Linux"
                        Puppet.notice "Using service type %s for %s" %
                            ["init", Facter["operatingsystem"].value]
                        @defsvctype = self.svctype(:init)
                    end
                end

                unless @defsvctype
                    Puppet.notice "Defaulting to base service type"
                    @defsvctype = self.svctype(:base)
                end
            end

            Puppet.debug "Default service type is %s" % @defsvctype.name

            return @defsvctype
        end

        # Execute a command.  Basically just makes sure it exits with a 0
        # code.
        def execute(type, cmd)
            self.info "Executing %s" % cmd.inspect
            output = %x(#{cmd} 2>&1)
            unless $? == 0
                self.fail "Could not %s %s: %s" %
                    [type, self.name, output.chomp]
            end
        end

        # Get the process ID for a running process. Requires the 'pattern'
        # parameter.
        def getpid
            unless self[:pattern]
                self.fail "Either a stop command or a pattern must be specified"
            end
            ps = Facter["ps"].value
            unless ps and ps != ""
                self.fail(
                    "You must upgrade Facter to a version that includes 'ps'"
                )
            end
            regex = Regexp.new(self[:pattern])
            IO.popen(ps) { |table|
                table.each { |line|
                    if regex.match(line)
                        ary = line.sub(/^\s+/, '').split(/\s+/)
                        return ary[1]
                    end
                }
            }

            return nil
        end

        # Initialize the service.  This is basically responsible for merging
        # in the right module.
        def initialize(hash)
            super

            # and then see if it needs to be checked
            if self.respond_to?(:configchk)
                self.configchk
            end
        end

        # Retrieve the service type.
        def type2module(type)
            self.class.svctype(type)
        end

        # Basically just a synonym for restarting.  Used to respond
        # to events.
        def refresh
            self.restart
        end

        # How to restart the process.
        def restart
            if self[:restart] or self.respond_to?(:restartcmd)
                cmd = self[:restart] || self.restartcmd
                self.execute("restart", cmd)
            else
                self.stop
                self.start
            end
        end

        # Check if the process is running.  Prefer the 'status' parameter,
        # then 'statuscmd' method, then look in the process table.  We give
        # the object the option to not return a status command, which might
        # happen if, for instance, it has an init script (and thus responds to
        # 'statuscmd') but does not have 'hasstatus' enabled.
        def status
            if self[:status] or (
                self.respond_to?(:statuscmd) and self.statuscmd
            )
                cmd = self[:status] || self.statuscmd
                self.info "Executing %s" % cmd.inspect
                output = %x(#{cmd} 2>&1)
                self.debug "%s status returned %s" %
                    [self.name, output.inspect]
                if $? == 0
                    return :running
                else
                    return :stopped
                end
            elsif pid = self.getpid
                return :running
            else
                return :stopped
            end
        end

        # Run the 'start' parameter command, or the specified 'startcmd'.
        def start
            cmd = self[:start] || self.startcmd
            self.execute("start", cmd)
        end

        # Stop the service.  If a 'stop' parameter is specified, it
        # takes precedence; otherwise checks if the object responds to
        # a 'stopcmd' method, and if so runs that; otherwise, looks
        # for the process in the process table.
        # This method will generally not be overridden by submodules.
        def stop
            if self[:stop]
                return self[:stop]
            elsif self.respond_to?(:stopcmd)
                self.execute("stop", self.stopcmd)
            else
                pid = getpid
                unless pid
                    self.info "%s is not running" % self.name
                    return false
                end
                output = %x("kill #{pid} 2>&1")
                if $? != 0
                    self.fail "Could not kill %s, PID %s: %s" %
                            [self.name, pid, output]
                end
                return true
            end
        end
    end
end

# Load all of the different service types.  We could probably get away with
# loading less here, but it's not a big deal to do so.
require 'puppet/type/service/base'
require 'puppet/type/service/init'
require 'puppet/type/service/debian'
require 'puppet/type/service/smf'

# $Id: service.rb 887 2006-02-08 22:56:56Z luke $