# == Synopsis
# The Daemon takes care of the work of creating and managing daemon
# processes from Ruby.
#
# == Details
# A daemon process is a long running process on a UNIX system that is
# detached from a TTY -- i.e. it is not tied to a user session. These types
# of processes are notoriously difficult to setup correctly. This Daemon
# class encapsulates some best practices to ensure daemons startup properly
# and can be shutdown gracefully.
#
# Starting a daemon process involves forking a child process, setting the
# child as a session leader, forking again, and detaching from the current
# working directory and standard in/out/error file descriptors. Because of
# this separation between the parent process and the daemon process, it is
# difficult to know if the daemon started properly.
#
# The Daemon class opens a pipe between the parent and the daemon. The PID
# of the daemon is sent to the parent through this pipe. The PID is used to
# check if the daemon is alive. Along with the PID, any errors from the
# daemon process are marshalled through the pipe back to the parent. These
# errors are wrapped in a StartupError and then raised in the parent.
#
# If no errors are passed up the pipe, the parent process waits till the
# daemon starts. This is determined by sending a signal to the daemon
# process.
#
# If a log file is given to the Daemon instance, then it is monitored for a
# change in size and mtime. This lets the Daemon instance know that the
# daemon process is updating the log file. Furthermore, the log file can be
# watched for a specific pattern; this pattern signals that the daemon
# process is up and running.
#
# Shutting down the daemon process is a little simpler. An external shutdown
# command can be used, or the Daemon instance will send an INT or TERM
# signal to the daemon process.
#
# Again, the Daemon instance will wait till the daemon process shuts down.
# This is determined by attempting to signal the daemon process PID and then
# returning when this signal fails -- i.e. then the deamon process has died.
#
# == Examples
#
# ==== Bad Example
# This is a bad example. The daemon will not start because the startup
# command "/usr/bin/no-command-by-this-name" cannot be found on the file
# system. The daemon process will send an Errno::ENOENT through the pipe
# back to the parent which gets wrapped in a StartupError
#
# daemon = Servolux::Daemon.new(
# :name => 'Bad Example',
# :pid_file => '/dev/null',
# :startup_command => '/usr/bin/no-command-by-this-name'
# )
# daemon.startup #=> raises StartupError
#
# ==== Good Example
# This is a simple Ruby server that prints the time to a file every minute.
# So, it's not really a "good" example, but it will work.
#
# server = Servolux::Server.new('TimeStamp', :interval => 60)
# class << server
# def file() @fd ||= File.open('timestamps.txt', 'w'); end
# def run() file.puts Time.now; end
# end
#
# daemon = Servolux::Daemon.new(:server => server, :log_file => 'timestamps.txt')
# daemon.startup
#
class Servolux::Daemon
Error = Class.new(::Servolux::Error)
Timeout = Class.new(Error)
StartupError = Class.new(Error)
attr_reader :name
attr_writer :logger
attr_accessor :pid_file
attr_reader :startup_command
attr_accessor :shutdown_command
attr_accessor :timeout
attr_accessor :nochdir
attr_accessor :noclose
attr_reader :log_file
attr_reader :look_for
# Create a new Daemon that will manage the +startup_command+ as a deamon
# process.
#
# ==== Required
# * name <String>
# The name of the daemon process. This name will appear in log
# messages.
#
# * logger <Logger>
# The Logger instance used to output messages.
#
# * pid_file <String>
# Location of the PID file. This is used to determine if the daemon
# process is running, and to send signals to the daemon process.
#
# * startup_command
# Assign the startup command. This can be either a String, an Array of
# strings, a Proc, a bound Method, or a Servolux::Server instance.
# Different calling semantics are used for each type of command. See
# the setter method for more details.
#
# ==== Options
#
# * timeout <Numeric>
# The time (in seconds) to wait for the daemon process to either
# startup or shutdown. An error is raised when this timeout is
# exceeded. The default is 30 seconds.
#
# * nochdir <Boolean>
# When set to true this flag directs the daemon process to keep the
# current working directory. By default, the process of daemonizing
# will cause the current working directory to be changed to the root
# folder (thus preventing the daemon process from holding onto the
# directory inode). The default is false.
#
# * noclose <Boolean>
# When set to true this flag keeps the standard input/output streams
# from being reopend to /dev/null when the deamon process is created.
# Reopening the standard input/output streams frees the file
# descriptors which are still being used by the parent process. This
# prevents zombie processes. The default is false.
#
# * shutdown_command
# Assign the startup command. This can be either a String, an Array of
# strings, a Proc, a bound Method, or a Servolux::Server instance.
# Different calling semantics are used for each type of command.
#
# * log_file <String>
# This log file will be monitored to determine if the daemon process
# has sucessfully started.
#
# * look_for
# This can be either a String or a Regexp. It defines a phrase to
# search for in the log_file. When the daemon process is started, the
# parent process will not return until this phrase is found in the log
# file. This is a useful check for determining if the daemon process
# is fully started. The default is nil.
#
def initialize( opts = {} )
self.server = opts.getopt(:server) || opts.getopt(:startup_command)
@name = opts[:name] if opts.key?(:name)
@logger = opts[:logger] if opts.key?(:logger)
@pid_file = opts[:pid_file] if opts.key?(:pid_file)
@timeout = opts.getopt(:timeout, 30)
@nochdir = opts.getopt(:nochdir, false)
@noclose = opts.getopt(:noclose, false)
@shutdown_command = opts.getopt(:shutdown_command)
@piper = nil
@logfile_reader = nil
self.log_file = opts.getopt(:log_file)
self.look_for = opts.getopt(:look_for)
yield self if block_given?
ary = %w[name logger pid_file startup_command].map { |var|
self.send(var).nil? ? var : nil
}.compact
raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
end
# Assign the startup command. This can be either a String, an Array of
# strings, a Proc, a bound Method, or a Servolux::Server instance.
# Different calling semantics are used for each type of command.
#
# If the startup command is a String or an Array of strings, then
# Kernel#exec is used to run the command. Therefore, the string (or array)
# should be system level command that is either fully qualified or can be
# found on the current environment path.
#
# If the startup command is a Proc or a bound Method then it is invoked
# using the +call+ method on the object. No arguments are passed to the
# +call+ invocoation.
#
# Lastly, if the startup command is a Servolux::Server then it's +startup+
# method is called.
#
def startup_command=( val )
@startup_command = val
return unless val.is_a?(::Servolux::Server)
@name = val.name
@logger = val.logger
@pid_file = val.pid_file
@shutdown_command = nil
end
alias :server= :startup_command=
alias :server :startup_command
# Assign the log file name. This log file will be monitored to determine
# if the daemon process is running.
#
def log_file=( filename )
return if filename.nil?
@logfile_reader ||= LogfileReader.new
@logfile_reader.filename = filename
end
# A string or regular expression to search for in the log file. When the
# daemon process is started, the parent process will not return until this
# phrase is found in the log file. This is a useful check for determining
# if the daemon process is fully started.
#
# If no phrase is given to look for, then the log file will simply be
# watched for a change in size and a modified timestamp.
#
def look_for=( val )
return if val.nil?
@logfile_reader ||= LogfileReader.new
@logfile_reader.look_for = val
end
# Start the daemon process.
#
def startup
raise Error, "Fork is not supported in this Ruby environment." unless ::Servolux.fork?
return if alive?
logger.debug "About to fork ..."
@piper = ::Servolux::Piper.daemon(nochdir, noclose)
@piper.parent {
@piper.timeout = 0
wait_for_startup
exit!(0)
}
@piper.child { run_startup_command }
end
# Stop the daemon process. If a shutdown command has been defined, it will
# be called to stop the daemon process. Otherwise, SIGINT will be sent to
# the daemon process to terminate it.
#
def shutdown
return unless alive?
case shutdown_command
when nil; kill
when String; exec(shutdown_command)
when Array; exec(*shutdown_command)
when Proc, Method; shutdown_command.call
when ::Servolux::Server; shutdown_command.shutdown
else
raise Error, "Unrecognized shutdown command #{shutdown_command.inspect}"
end
wait_for_shutdown
end
# Returns +true+ if the daemon processis currently running. Returns
# +false+ if this is not the case. The status of the process is determined
# by sending a signal to the process identified by the +pid_file+.
#
def alive?
pid = retrieve_pid
Process.kill(0, pid)
true
rescue Errno::ESRCH, Errno::ENOENT
false
rescue Errno::EACCES => err
logger.error "You do not have access to the PID file at " \
"#{pid_file.inspect}: #{err.message}"
false
end
# Send a signal to the daemon process identified by the PID file. The
# default signal to send is 'INT' (2). The signal can be given either as a
# string or a signal number.
#
def kill( signal = 'INT' )
signal = Signal.list.invert[signal] if signal.is_a?(Integer)
pid = retrieve_pid
logger.info "Killing PID #{pid} with #{signal}"
Process.kill(signal, pid)
rescue Errno::EINVAL
logger.error "Failed to kill PID #{pid} with #{signal}: " \
"'#{signal}' is an invalid or unsupported signal number."
rescue Errno::EPERM
logger.error "Failed to kill PID #{pid} with #{signal}: " \
"Insufficient permissions."
rescue Errno::ESRCH
logger.error "Failed to kill PID #{pid} with #{signal}: " \
"Process is deceased or zombie."
rescue Errno::EACCES => err
logger.error err.message
rescue Errno::ENOENT => err
logger.error "Could not find a PID file at #{pid_file.inspect}. " \
"Most likely the process is no longer running."
rescue Exception => err
unless err.is_a?(SystemExit)
logger.error "Failed to kill PID #{pid} with #{signal}: #{err.message}"
end
end
# Returns the logger instance used by the daemon to log messages.
#
def logger
@logger ||= Logging.logger[self]
end
private
def run_startup_command
case startup_command
when String; exec(startup_command)
when Array; exec(*startup_command)
when Proc, Method; startup_command.call
when ::Servolux::Server; startup_command.startup
else
raise Error, "Unrecognized startup command #{startup_command.inspect}"
end
rescue Exception => err
unless err.is_a?(SystemExit)
logger.fatal err
@piper.puts err
end
ensure
@piper.close
end
def exec( *args )
logger.debug "Calling: exec(*#{args.inspect})"
skip = [STDIN, STDOUT, STDERR]
skip << @piper.write_io if @piper
ObjectSpace.each_object(IO) { |obj|
next if skip.include? obj
obj.close rescue nil
}
Kernel.exec(*args)
end
def retrieve_pid
@piper ? @piper.pid : Integer(File.read(pid_file).strip)
rescue TypeError
raise Error, "A PID file was not specified."
rescue ArgumentError
raise Error, "#{pid_file.inspect} does not contain a valid PID."
end
def started?
return false unless alive?
return true if @logfile_reader.nil?
@logfile_reader.updated?
end
def wait_for_startup
logger.debug "Waiting for #{name.inspect} to startup."
started = wait_for {
rv = started?
err = @piper.gets
raise StartupError, "Child raised error: #{err.inspect}" unless err.nil?
rv
}
raise Timeout, "#{name.inspect} failed to startup in a timely fashion. " \
"The timeout is set at #{timeout} seconds." unless started
logger.info 'Server has daemonized.'
ensure
@piper.close
end
def wait_for_shutdown
logger.debug "Waiting for #{name.inspect} to shutdown."
return if wait_for { !alive? }
raise Timeout, "#{name.inspect} failed to shutdown in a timely fashion. " \
"The timeout is set at #{timeout} seconds."
end
def wait_for
start = Time.now
nap_time = 0.2
loop do
sleep nap_time
diff = Time.now - start
nap_time = 2*nap_time
nap_time = 0.2 if nap_time > 1.6
break true if yield
break false if diff >= timeout
end
end
# :stopdoc:
class LogfileReader
attr_accessor :filename
attr_reader :look_for
def look_for=( val )
case val
when nil; @look_for = nil
when String; @look_for = Regexp.new(Regexp.escape(val))
when Regexp; @look_for = val
else
raise Error,
"Don't know how to look for #{val.inspect} in the logfile"
end
end
def stat
if @filename and test(?f, @filename)
File.stat @filename
end
end
def updated?
s = stat
@stat ||= s
return false if s.nil?
return false if @stat.mtime == s.mtime and @stat.size == s.size
return true if @look_for.nil?
File.open(@filename, 'r') do |fd|
fd.seek @stat.size, IO::SEEK_SET
while line = fd.gets
return true if line =~ @look_for
end
end
return false
ensure
@stat = s
end
end # class LogfileReader
# :startdoc:
end # class Servolux::Daemon
# EOF