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

Defined Under Namespace

Classes: LogfileReader

Constant Summary collapse

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) {|self| ... } ⇒ Daemon

Create a new Daemon that will manage the startup_command as a deamon 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 (Boolen) — 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 (Boolen) — default: false

    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.

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

    Assign the startup 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 sucessfully 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.

Yields:

  • (self)

    Block used to configure the daemon instance

Raises:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/servolux/daemon.rb', line 143

def initialize( opts = {} )
  self.server = opts[:server] || opts[: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[:timeout] || 30
  @nochdir  = opts[:nochdir] || false
  @noclose  = opts[:noclose] || false
  @shutdown_command = opts[:shutdown_command]

  @piper = nil
  @logfile_reader = nil
  self.log_file = opts[:log_file]
  self.look_for = opts[: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

Instance Attribute Details

#log_fileObject

Returns the value of attribute log_file.



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

def log_file
  @log_file
end

#loggerObject

Returns the value of attribute logger.



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

def logger
  @logger
end

#look_forObject

Returns the value of attribute look_for.



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

def look_for
  @look_for
end

#nameObject (readonly)

Returns the value of attribute name.



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

def name
  @name
end

#nochdirObject

Returns the value of attribute nochdir.



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

def nochdir
  @nochdir
end

#nocloseObject

Returns the value of attribute noclose.



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

def noclose
  @noclose
end

#pid_fileObject

Returns the value of attribute pid_file.



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

def pid_file
  @pid_file
end

#shutdown_commandObject

Returns the value of attribute shutdown_command.



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

def shutdown_command
  @shutdown_command
end

#startup_commandObject 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

#timeoutObject

Returns the value of attribute timeout.



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

def timeout
  @timeout
end

Instance Method Details

#alive?Boolean

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)


280
281
282
283
284
285
286
287
288
289
290
# File 'lib/servolux/daemon.rb', line 280

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

#kill(signal = 'INT') ⇒ Daemon

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:



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/servolux/daemon.rb', line 300

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

#shutdownDaemon

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:



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/servolux/daemon.rb', line 257

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) ⇒ Daemon

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

Returns:

Raises:



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/servolux/daemon.rb', line 230

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