class Servolux::Daemon

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

Constants

Error
StartupError
Timeout

Attributes

after_fork[RW]
before_exec[RW]
log_file[R]
logger[RW]
look_for[R]
name[RW]
nochdir[RW]
noclose[RW]
pid_file[R]
server[R]
shutdown_command[RW]
startup_command[R]
timeout[RW]

Public Class Methods

new( opts = {} ) { |self| ... } click to toggle source

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

# File lib/servolux/daemon.rb, line 153
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

Public Instance Methods

alive?() click to toggle source

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]

# File lib/servolux/daemon.rb, line 314
def alive?
  pid_file.alive?
end
kill( signal = 'INT' ) click to toggle source

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

# File lib/servolux/daemon.rb, line 326
def kill( signal = 'INT' )
  pid_file.kill signal
end
log_file=( filename ) click to toggle source

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

# File lib/servolux/daemon.rb, line 237
def log_file=( filename )
  return if filename.nil?
  @logfile_reader ||= LogfileReader.new
  @logfile_reader.filename = filename
end
look_for=( val ) click to toggle source

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

# File lib/servolux/daemon.rb, line 253
def look_for=( val )
  return if val.nil?
  @logfile_reader ||= LogfileReader.new
  @logfile_reader.look_for = val
end
pid_file=( value ) click to toggle source

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.

# File lib/servolux/daemon.rb, line 218
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
server=( val ) click to toggle source
Alias for: startup_command=
shutdown() click to toggle source

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

# File lib/servolux/daemon.rb, line 291
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
startup( do_exit = true ) click to toggle source

Start the daemon process. Passing in false to this method will prevent the parent from exiting after the daemon process starts.

@return [Daemon] self

# File lib/servolux/daemon.rb, line 264
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
startup_command=( val ) click to toggle source

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.

# File lib/servolux/daemon.rb, line 199
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
Also aliased as: server=