Class: Servolux::Daemon
- Inherits:
-
Object
- Object
- Servolux::Daemon
- 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)
-
- (Object) after_fork
Returns the value of attribute after_fork.
-
- (Object) before_exec
Returns the value of attribute before_exec.
-
- (Object) log_file
Returns the value of attribute log_file.
-
- (Object) logger
Returns the value of attribute logger.
-
- (Object) look_for
Returns the value of attribute look_for.
-
- (Object) name
Returns the value of attribute name.
-
- (Object) nochdir
Returns the value of attribute nochdir.
-
- (Object) noclose
Returns the value of attribute noclose.
-
- (Object) pid_file
Returns the value of attribute pid_file.
-
- (Object) shutdown_command
Returns the value of attribute shutdown_command.
-
- (Object) startup_command
(also: #server)
Returns the value of attribute startup_command.
-
- (Object) timeout
Returns the value of attribute timeout.
Instance Method Summary (collapse)
-
- (Boolean) alive?
Returns true if the daemon process is currently running.
-
- (Daemon) initialize(opts = {}) {|self| ... }
constructor
Create a new Daemon that will manage the startup_command as a daemon process.
-
- (Daemon) kill(signal = 'INT')
Send a signal to the daemon process identified by the PID file.
-
- (Daemon) shutdown
Stop the daemon process.
-
- (Daemon) startup(do_exit = true)
Start the daemon process.
Constructor Details
- (Daemon) initialize(opts = {}) {|self| ... }
Create a new Daemon that will manage the startup_command as a daemon process.
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.
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.}" 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.
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. 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.}" 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.
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.
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 |