class Servolux::Daemon


daemon.startup
daemon = Servolux::Daemon.new(:server => server, :log_file => ‘timestamps.txt’)
end
def run() file.puts Time.now; end
def file() @fd ||= File.open(‘timestamps.txt’, ‘w’); end
class << server
server = Servolux::Server.new(‘TimeStamp’, :interval => 60)
So, it’s not really a “good” example, but it will work.
This is a simple Ruby server that prints the time to a file every minute.
==== Good Example
daemon.startup #=> raises StartupError
)
:startup_command => ‘/usr/bin/no-command-by-this-name’
:pid_file => ‘/dev/null’,
:name => ‘Bad Example’,
daemon = Servolux::Daemon.new(
back to the parent which gets wrapped in a StartupError
system. The daemon process will send an Errno::ENOENT through the pipe
command “/usr/bin/no-command-by-this-name” cannot be found on the file
This is a bad example. The daemon will not start because the startup
==== Bad Example
== Examples
returning when this signal fails – i.e. then the deamon process has died.
This is determined by attempting to signal the daemon process PID and then
Again, the Daemon instance will wait till the daemon process shuts down.
signal to the daemon process.
command can be used, or the Daemon instance will send an INT or TERM
Shutting down the daemon process is a little simpler. An external shutdown
process is up and running.
watched for a specific pattern; this pattern signals that the daemon
daemon process is updating the log file. Furthermore, the log file can be
change in size and mtime. This lets the Daemon instance know that the
If a log file is given to the Daemon instance, then it is monitored for a
process.
daemon starts. This is determined by sending a signal to the daemon
If no errors are passed up the pipe, the parent process waits till the
errors are wrapped in a StartupError and then raised in the parent.
daemon process are marshalled through the pipe back to the parent. These
check if the daemon is alive. Along with the PID, any errors from the
of the daemon is sent to the parent through this pipe. The PID is used to
The Daemon class opens a pipe between the parent and the daemon. The PID
difficult to know if the daemon started properly.
this separation between the parent process and the daemon process, it is
working directory and standard in/out/error file descriptors. Because of
child as a session leader, forking again, and detaching from the current
Starting a daemon process involves forking a child process, setting the
and can be shutdown gracefully.
class encapsulates some best practices to ensure daemons startup properly
of processes are notoriously difficult to setup correctly. This Daemon
detached from a TTY – i.e. it is not tied to a user session. These types
A daemon process is a long running process on a UNIX system that is
== Details
processes from Ruby.
The Daemon takes care of the work of creating and managing daemon
== Synopsis

def alive?


by sending a signal to the process identified by the +pid_file+.
+false+ if this is not the case. The status of the process is determined
Returns +true+ if the daemon processis currently running. Returns
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

def exec( *args )

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 initialize( opts = {} )


is fully started. The default is nil.
file. This is a useful check for determining if the daemon process
parent process will not return until this phrase is found in the log
search for in the log_file. When the daemon process is started, the
This can be either a String or a Regexp. It defines a phrase to
* look_for

has sucessfully started.
This log file will be monitored to determine if the daemon process
* log_file

Different calling semantics are used for each type of command.
strings, a Proc, a bound Method, or a Servolux::Server instance.
Assign the startup command. This can be either a String, an Array of
* shutdown_command

prevents zombie processes. The default is false.
descriptors which are still being used by the parent process. This
Reopening the standard input/output streams frees the file
from being reopend to /dev/null when the deamon process is created.
When set to true this flag keeps the standard input/output streams
* noclose

directory inode). The default is false.
folder (thus preventing the daemon process from holding onto the
will cause the current working directory to be changed to the root
current working directory. By default, the process of daemonizing
When set to true this flag directs the daemon process to keep the
* nochdir

exceeded. The default is 30 seconds.
startup or shutdown. An error is raised when this timeout is
The time (in seconds) to wait for the daemon process to either
* timeout

==== Options

the setter method for more details.
Different calling semantics are used for each type of command. See
strings, a Proc, a bound Method, or a Servolux::Server instance.
Assign the startup command. This can be either a String, an Array of
* startup_command

process is running, and to send signals to the daemon process.
Location of the PID file. This is used to determine if the daemon
* pid_file

The Logger instance used to output messages.
* logger

messages.
The name of the daemon process. This name will appear in log
* name
==== Required

process.
Create a new Daemon that will manage the +startup_command+ as a deamon
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

def kill( signal = 'INT' )


string or a signal number.
default signal to send is 'INT' (2). The signal can be given either as a
Send a signal to the daemon process identified by the PID file. The
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

def log_file=( filename )


if the daemon process is running.
Assign the log file name. This log file will be monitored to determine
def log_file=( filename )
  return if filename.nil?
  @logfile_reader ||= LogfileReader.new
  @logfile_reader.filename = filename
end

def logger


Returns the logger instance used by the daemon to log messages.
def logger
  @logger ||= Logging.logger[self]
end

def look_for=( val )


watched for a change in size and a modified timestamp.
If no phrase is given to look for, then the log file will simply be

if the daemon process is fully started.
phrase is found in the log file. This is a useful check for determining
daemon process is started, the parent process will not return until this
A string or regular expression to search for in the log file. When the
def look_for=( val )
  return if val.nil?
  @logfile_reader ||= LogfileReader.new
  @logfile_reader.look_for = val
end

def retrieve_pid

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 run_startup_command

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 shutdown


the daemon process to terminate it.
be called to stop the daemon process. Otherwise, SIGINT will be sent to
Stop the daemon process. If a shutdown command has been defined, it will
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

def started?

def started?
  return false unless alive?
  return true if @logfile_reader.nil?
  @logfile_reader.updated?
end

def startup


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

def startup_command=( val )


method is called.
Lastly, if the startup command is a Servolux::Server then it's +startup+

+call+ invocoation.
using the +call+ method on the object. No arguments are passed to the
If the startup command is a Proc or a bound Method then it is invoked

found on the current environment path.
should be system level command that is either fully qualified or can be
Kernel#exec is used to run the command. Therefore, the string (or array)
If the startup command is a String or an Array of strings, then

Different calling semantics are used for each type of command.
strings, a Proc, a bound Method, or a Servolux::Server instance.
Assign the startup command. This can be either a String, an Array of
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

def wait_for

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

def wait_for_shutdown

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_startup

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