require 'optparse'
require 'timeout'
module Daemoned
class DieTime < StandardError; end
class TimeoutError < StandardError; end
def self.included(base)
base.extend ClassMethods
base.initialize_options
end
class Config
METHODS = [:script_path]
CONFIG = {}
def method_missing(name, *args)
name = name.to_s.upcase.to_sym
if name.to_s =~ /^(.*)=$/
name = $1.to_sym
CONFIG[name] = args.first
else
CONFIG[name]
end
end
end
module ClassMethods
def initialize_options
@@config = Config.new
@@config.script_path = File.expand_path(File.dirname($0))
$0 = script_name
end
def parse_options
opts = OptionParser.new do |opt|
opt.banner = "Usage: #{script_name} [options] [start|stop]"
opt.on_tail('-h', '--help', 'Show this message') do
puts opt
exit(1)
end
opt.on('--loop-every=SECONDS', 'How long to sleep between each loop') do |value|
options[:loop_every] = value
end
opt.on('-t', '--ontop', 'Stay on top (does not daemonize)') do
options[:ontop] = true
end
opt.on('--log-file=LOGFILE', 'Logfile to log to') do |value|
options[:log_file] = File.expand_path(value)
end
opt.on('--pid-file=PIDFILE', 'Location of pidfile') do |value|
options[:pid_file] = File.expand_path(value)
end
opt.on('--no-log-prefix', 'Do not prefix PID and date/time in log file.') do
options[:log_prefix] = false
end
end
extra_args.each do |arg|
opts.on(*arg.first) do |value|
arg.last.call(value) if arg.last
end
end
opts.parse!
if ARGV.include?('stop')
stop
elsif ARGV.include?('reload')
kill('HUP')
exit
elsif not ARGV.include?('start') and not ontop?
puts opts.help
end
end
def arg(*args, &block)
self.extra_args << [args, block]
end
def extra_args
@extra_args ||= []
end
def callbacks
@callbacks ||= {}
end
def options
@options ||= {}
end
def options=(options)
@options = options
end
def config
yield @@config
end
def before(&block)
callbacks[:before] = block
end
def after(&block)
callbacks[:after] = block
end
def sig(*signals, &block)
signals.each do |s|
callbacks["sig_#{s}".to_sym] = block
end
end
def die_if(method=nil,&block)
options[:die_if] = method || block
end
def exit_if(method=nil,&block)
options[:exit_if] = method || block
end
def callback!(callback)
callbacks[callback].call if callbacks[callback]
end
# options may include:
#
# :loop_every Fixnum (DEFAULT 0)
# How many seconds to sleep between calls to your block
#
# :timeout Fixnum (DEFAULT 0)
# Timeout in if block does not execute withing passed number of seconds
#
# :kill_timeout Fixnum (DEFAULT 120)
# Wait number of seconds before using kill -9 on daemon
#
# :die_on_timeout BOOL (DEFAULT False)
# Should the daemon continue running if a block times out, or just run the block again
#
# :ontop BOOL (DEFAULT False)
# Do not daemonize. Run in current process
#
# :before BLOCK
# Run this block after daemonizing but before begining the daemonize loop.
# You can also define the before block by putting a before do/end block in your class.
#
# :after BLOCK
# Run this block before program exists.
# You can also define the after block by putting an after do/end block in your class.
#
# :die_if BLOCK
# Run this check after each iteration of the loop. If the block returns true, throw a DieTime exception and exit
# You can also define the after block by putting an die_if do/end block in your class.
#
# :exit_if BLOCK
# Run this check after each iteration of the loop. If the block returns true, exit gracefully
# You can also define the after block by putting an exit_if do/end block in your class.
#
# :log_prefix BOOL (DEFAULT true)
# Prefix log file entries with PID and timestamp
def daemonize(opts={}, &block)
self.options = opts
parse_options
return unless ok_to_start?
puts "Starting #{script_name}..."
puts "Logging to: #{log_file}" unless ontop?
unless ontop?
safefork do
open(pid_file, 'w'){|f| f << Process.pid }
at_exit { remove_pid! }
trap('TERM') { callback!(:sig_term) }
trap('INT') { callback!(:sig_int) ; Process.kill('TERM', $$) }
trap('HUP') { callback!(:sig_hup) }
sess_id = Process.setsid
reopen_filehandes
begin
at_exit { callback!(:after) }
callback!(:before)
run_block(&block)
rescue SystemExit
rescue Exception => e
$stdout.puts "Something bad happened #{e.inspect} #{e.backtrace.join("\n")}"
end
end
else
begin
callback!(:before)
run_block(&block)
rescue SystemExit, Interrupt
callback!(:after)
end
end
end
private
def run_block(&block)
loop do
if options[:timeout]
begin
Timeout::timeout(options[:timeout].to_i) do
block.call if block
end
rescue Timeout::Error => e
if options[:die_on_timeout]
raise TimeoutError.new("#{self} timed out after #{options[:timeout]} seconds while executing block in loop")
else
$stderr.puts "#{self} timed out after #{options[:timeout]} seconds while executing block in loop #{e.backtrace.join("\n")}"
end
end
else
block.call if block
end
if options[:loop_every]
sleep options[:loop_every].to_i
elsif not block
sleep 0.1
end
break if should_exit?
raise DieTime.new('Die if conditions were met!') if should_die?
end
exit(0)
end
def should_die?
die_if = options[:die_if]
if die_if
if die_if.is_a?(Symbol) or die_if.is_a?(String)
self.send(die_if)
elsif die_if.is_a?(Proc)
die_if.call
end
else
false
end
end
def should_exit?
exit_if = options[:exit_if]
if exit_if
if exit_if.is_a?(Symbol) or exit_if.is_a?(String)
self.send(exit_if.to_sym)
elsif exit_if.is_a?(Proc)
exit_if.call
end
else
false
end
end
def ok_to_start?
return true if pid.nil?
if process_alive?
$stderr.puts "#{script_name} is already running"
return false
else
$stderr.puts "Removing stale pid: #{pid}..."
end
true
end
def stop
puts "Stopping #{script_name}..."
kill
exit
end
def kill(signal = 'TERM')
if pid.nil?
$stderr.puts "#{script_name} doesn't appear to be running"
exit(1)
end
$stdout.puts("Sending pid #{pid} signal #{signal}...")
begin
Process.kill(signal, pid)
return if signal == 'HUP'
if pid_running?(options[:kill_timeout] || 120)
$stdout.puts("Using kill -9 #{pid}")
Process.kill(9, pid)
else
$stdout.puts("Process #{pid} stopped")
end
rescue Errno::ESRCH
$stdout.puts("Couldn't #{signal} #{pid} as it wasn't running")
end
end
def pid_running?(time_to_wait = 0)
times_to_check = 1
if time_to_wait > 0.5
times_to_check = (time_to_wait / 0.5).to_i
end
times_to_check.times do
return false unless process_alive?
sleep 0.5
end
true
end
def safefork(&block)
fork_tries ||= 0
fork(&block)
rescue Errno::EWOULDBLOCK
raise if fork_tries >= 20
fork_tries += 1
sleep 5
retry
end
def process_alive?
Process.kill(0, pid)
true
rescue Errno::ESRCH => e
false
end
LOG_FORMAT = '%-6d %-19s %s'
TIME_FORMAT = '%Y/%m/%d %H:%M:%S'
def reopen_filehandes
STDIN.reopen('/dev/null')
STDOUT.reopen(log_file, 'a')
STDOUT.sync = true
STDERR.reopen(STDOUT)
if log_prefix?
def STDOUT.write(string)
if @no_prefix
@no_prefix = false if string[-1, 1] == "\n"
else
string = LOG_FORMAT % [$$, Time.now.strftime(TIME_FORMAT), string]
@no_prefix = true
end
super(string)
end
end
end
def remove_pid!
if File.file?(pid_file) and File.read(pid_file).to_i == $$
File.unlink(pid_file)
end
end
def ontop?
options[:ontop]
end
def log_prefix?
options[:log_prefix] || true
end
LOG_PATHS = ['log/', 'logs/', '../log/', '../logs/', '../../log', '../../logs', '.']
LOG_PATHS.unshift("#{RAILS_ROOT}/log") if defined?(RAILS_ROOT)
def log_dir
options[:log_dir] ||= begin
LOG_PATHS.detect do |path|
File.exists?(File.expand_path(path))
end
end
end
def log_file
options[:log_file] ||= File.expand_path("#{log_dir}/#{script_name}.log")
end
def pid_dir
options[:pid_dir] ||= log_dir
end
def pid_file
options[:pid_file] ||= File.expand_path("#{pid_dir}/#{script_name}.pid")
end
def pid
@pid ||= File.file?(pid_file) ? File.read(pid_file).to_i : nil
end
def script_name
@script_name ||= File.basename($0).gsub('.rb', '')
end
def script_name=(script_name)
@script_name = script_name
end
end
end