require 'ostruct'
# == 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 daemon 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_accessor :name
attr_accessor :logger
attr_reader :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
attr_accessor :after_fork
attr_accessor :before_exec
# Create a new Daemon that will manage the +startup_command+ as a daemon
# process.
#
# @option opts [String] :name
# The name of the daemon process. This name will appear in log messages.
# [required]
#
# @option opts [Logger] :logger
# The Logger instance used to output messages. [required]
#
# @option opts [String] :pid_file
# Location of the PID file. This is used to determine if the daemon
# process is running, and to send signals to the daemon process.
# [required]
#
# @option opts [String, Array<String>, Proc, Method, Servolux::Server] :startup_command
# Assign the startup command. Different calling semantics are used for
# each type of command. See the {Daemon#startup_command= startup_command}
# method for more details. [required]
#
# @option opts [Numeric] :timeout (30)
# The time (in seconds) to wait for the daemon process to either startup
# or shutdown. An error is raised when this timeout is exceeded.
#
# @option opts [Boolean] :nochdir (false)
# 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).
#
# @option opts [Boolean] :noclose (false)
# When set to true this flag keeps the standard input/output streams from
# being reopened to /dev/null when the daemon 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.
#
# @option opts [Numeric, String, Array<String>, Proc, Method, Servolux::Server] :shutdown_command (nil)
# Assign the shutdown command. Different calling semantics are used for
# each type of command.
#
# @option opts [String] :log_file (nil)
# This log file will be monitored to determine if the daemon process has
# successfully started.
#
# @option opts [String, Regexp] :look_for (nil)
# 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.
#
# @option opts [Proc, lambda] :after_fork (nil)
# This proc will be called in the child process immediately after forking.
#
# @option opts [Proc, lambda] :before_exec (nil)
# This proc will be called in the child process immediately before calling
# `exec` to execute the desired process. This proc will be called after
# the :after_fork proc if present.
#
# @yield [self] Block used to configure the daemon instance
#
def initialize( opts = {} )
@piper = nil
@logfile_reader = nil
@pid_file = nil
self.name = opts.fetch(:name, nil)
self.logger = opts.fetch(:logger, Servolux::NullLogger())
self.startup_command = opts.fetch(:server, nil) || opts.fetch(:startup_command, nil)
self.shutdown_command = opts.fetch(:shutdown_command, nil)
self.timeout = opts.fetch(:timeout, 30)
self.nochdir = opts.fetch(:nochdir, false)
self.noclose = opts.fetch(:noclose, false)
self.log_file = opts.fetch(:log_file, nil)
self.look_for = opts.fetch(:look_for, nil)
self.after_fork = opts.fetch(:after_fork, nil)
self.before_exec = opts.fetch(:before_exec, nil)
self.pid_file = opts.fetch(:pid_file, name) if pid_file.nil?
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 a system 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+ invocation.
#
# Lastly, if the startup command is a Servolux::Server then its +startup+
# method is called.
#
# @param [String, Array<String>, Proc, Method, Servolux::Server] val The startup
# command to invoke when daemonizing.
#
def startup_command=( val )
@startup_command = val
return unless val.is_a?(::Servolux::Server)
self.name = val.name
self.logger = val.logger
self.pid_file = val.pid_file
@shutdown_command = nil
end
alias :server= :startup_command=
alias :server :startup_command
# Set the PID file to the given `value`. If a PidFile instance is given, then
# it is used. If a name is given, then that name is used to create a PifFile
# instance.
#
# value - The PID file name or a PidFile instance.
#
# Raises an ArgumentError if the `value` cannot be used as a PID file.
def pid_file=( value )
@pid_file =
case value
when Servolux::PidFile
value
when String
path = File.dirname(value)
fn = File.basename(value, ".pid")
Servolux::PidFile.new(:name => fn, :path => path, :logger => logger)
else
raise ArgumentError, "#{value.inspect} cannot be used as a PID file"
end
end
# Assign the log file name. This log file will be monitored to determine
# if the daemon process is running.
#
# @param [String] filename The name of the log file to monitor
#
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.
#
# @param [String, Regexp] val The phrase in the log file to search for
#
def look_for=( val )
return if val.nil?
@logfile_reader ||= LogfileReader.new
@logfile_reader.look_for = val
end
# Start the daemon process. Passing in +false+ to this method will prevent
# the parent from exiting after the daemon process starts.
#
# @return [Daemon] self
#
def startup( do_exit = true )
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)
# Make sure we have an idea of the state of the log file BEFORE the child
# gets a chance to write to it.
@logfile_reader.updated? if @logfile_reader
@piper.parent {
@piper.timeout = 0.1
wait_for_startup
exit!(0) if do_exit
}
@piper.child { run_startup_command }
self
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.
#
# @return [Daemon] self
#
def shutdown
return unless alive?
case shutdown_command
when nil; kill
when Integer; kill(shutdown_command)
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 process is 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+.
#
# @return [Boolean]
#
def alive?
pid_file.alive?
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.
#
# @param [String, Integer] signal The kill signal to send to the daemon
# process
# @return [Daemon] self
#
def kill( signal = 'INT' )
pid_file.kill signal
end
private
def run_startup_command
after_fork.call if after_fork.respond_to? :call
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.socket if @piper
ObjectSpace.each_object(IO) { |io|
next if skip.include? io
io.close unless io.closed?
}
before_exec.call if before_exec.respond_to? :call
Kernel.exec(*args)
end
def retrieve_pid
@piper ? @piper.pid : pid_file.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}", err.backtrace 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 self 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:
# @private
class LogfileReader
attr_accessor :filename
attr_reader :look_for
def initialize
@filename = nil
@look_for = nil
end
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
s = File.stat(@filename) if @filename && test(?f, @filename)
s || OpenStruct.new(:mtime => Time.at(0), :size => 0)
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
# :startdoc:
end