Class: Servolux::Daemon

Inherits:
Object
  • Object
show all
Defined in:
lib/servolux/daemon.rb

Overview

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

Constant Summary

Error =
Class.new(::Servolux::Error)
Timeout =
Class.new(Error)
StartupError =
Class.new(Error)

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Daemon) initialize(opts = {}) {|self| ... }

Create a new Daemon that will manage the startup_command as a daemon process.

Parameters:

  • opts (Hash) (defaults to: {})

    a customizable set of options

Options Hash (opts):

  • :name (String)

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

    required
  • :logger (Logger)

    The Logger instance used to output messages. [required]

  • :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.

    required
  • :startup_command (String, Array<String>, Proc, Method, Servolux::Server)

    Assign the startup command. Different calling semantics are used for each type of command. See the startup_command method for more details. [required]

  • :timeout (Numeric) — default: 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.

  • :nochdir (Boolean) — default: 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).

  • :noclose (Boolean) — default: 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.

  • :shutdown_command (Numeric, String, Array<String>, Proc, Method, Servolux::Server) — default: nil

    Assign the shutdown command. Different calling semantics are used for each type of command.

  • :log_file (String) — default: nil

    This log file will be monitored to determine if the daemon process has successfully started.

  • :look_for (String, Regexp) — default: 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.

  • :after_fork (Proc, lambda) — default: nil

    This proc will be called in the child process immediately after forking.

  • :before_exec (Proc, lambda) — default: 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.

Yields:

  • (self)

    Block used to configure the daemon instance

Raises:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/servolux/daemon.rb', line 153

def initialize( opts = {} )
  @piper = nil
  @logfile_reader = nil

  self.name     = opts.fetch(:name, nil)
  self.logger   = opts.fetch(:logger, nil)
  self.pid_file = opts.fetch(:pid_file, nil)
  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)

  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

Instance Attribute Details

- (Object) after_fork

Returns the value of attribute after_fork



88
89
90
# File 'lib/servolux/daemon.rb', line 88

def after_fork
  @after_fork
end

- (Object) before_exec

Returns the value of attribute before_exec



89
90
91
# File 'lib/servolux/daemon.rb', line 89

def before_exec
  @before_exec
end

- (Object) log_file

Returns the value of attribute log_file



86
87
88
# File 'lib/servolux/daemon.rb', line 86

def log_file
  @log_file
end

- (Object) logger

Returns the value of attribute logger



79
80
81
# File 'lib/servolux/daemon.rb', line 79

def logger
  @logger
end

- (Object) look_for

Returns the value of attribute look_for



87
88
89
# File 'lib/servolux/daemon.rb', line 87

def look_for
  @look_for
end

- (Object) name

Returns the value of attribute name



78
79
80
# File 'lib/servolux/daemon.rb', line 78

def name
  @name
end

- (Object) nochdir

Returns the value of attribute nochdir



84
85
86
# File 'lib/servolux/daemon.rb', line 84

def nochdir
  @nochdir
end

- (Object) noclose

Returns the value of attribute noclose



85
86
87
# File 'lib/servolux/daemon.rb', line 85

def noclose
  @noclose
end

- (Object) pid_file

Returns the value of attribute pid_file



80
81
82
# File 'lib/servolux/daemon.rb', line 80

def pid_file
  @pid_file
end

- (Object) shutdown_command

Returns the value of attribute shutdown_command



82
83
84
# File 'lib/servolux/daemon.rb', line 82

def shutdown_command
  @shutdown_command
end

- (Object) startup_command Also known as: server

Returns the value of attribute startup_command



81
82
83
# File 'lib/servolux/daemon.rb', line 81

def startup_command
  @startup_command
end

- (Object) timeout

Returns the value of attribute timeout



83
84
85
# File 'lib/servolux/daemon.rb', line 83

def timeout
  @timeout
end

Instance Method Details

- (Boolean) alive?

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.

Returns:

  • (Boolean)


291
292
293
294
295
296
297
298
299
300
301
# File 'lib/servolux/daemon.rb', line 291

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

- (Daemon) kill(signal = 'INT')

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.

Parameters:

  • signal (String, Integer) (defaults to: 'INT')

    The kill signal to send to the daemon process

Returns:



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/servolux/daemon.rb', line 311

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)
  self
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

- (Daemon) shutdown

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.

Returns:



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/servolux/daemon.rb', line 268

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

- (Daemon) startup(do_exit = true)

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

Returns:

Raises:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/servolux/daemon.rb', line 241

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