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