require 'daemons/pidfile'
require 'daemons/pidmem'
require 'daemons/change_privilege'
require 'daemons/daemonize'
require 'daemons/exceptions'
require 'daemons/reporter'
require 'timeout'
module Daemons
class Application
attr_accessor :app_argv
attr_accessor :controller_argv
# the Pid instance belonging to this application
attr_reader :pid
# the ApplicationGroup the application belongs to
attr_reader :group
# my private options
attr_reader :options
SIGNAL = (RUBY_PLATFORM =~ /win32/ ? 'KILL' : 'TERM')
def initialize(group, add_options = {}, pid = nil)
@group = group
@options = group.options.dup
@options.update(add_options)
['dir', 'log_dir', 'logfilename', 'output_logfilename'].each do |k|
@options[k] = File.expand_path(@options[k]) if @options.key?(k)
end
@dir_mode = @dir = @script = nil
@force_kill_waittime = @options[:force_kill_waittime] || 20
@signals_and_waits = parse_signals_and_waits(@options[:signals_and_waits])
@show_status_callback = method(:default_show_status)
@report = Reporter.new(@options)
unless @pid = pid
if @options[:no_pidfiles]
@pid = PidMem.new
elsif dir = pidfile_dir
@pid = PidFile.new(dir, @group.app_name, @group.multiple, @options[:pid_delimiter])
else
@pid = PidMem.new
end
end
end
def show_status_callback=(function)
@show_status_callback =
if function.respond_to?(:call)
function
else
method(function)
end
end
def change_privilege
user = options[:user]
group = options[:group]
if user
@report.changing_process_privilege(user, group)
CurrentProcess.change_privilege(user, group)
end
end
def script
@script or group.script
end
def pidfile_dir
Pid.dir dir_mode, dir, script
end
def logdir
options[:log_dir] or
options[:dir_mode] == :system ? '/var/log' : pidfile_dir
end
def output_logfilename
options[:output_logfilename] or "#{@group.app_name}.output"
end
def output_logfile
if log_output_syslog?
'SYSLOG'
elsif log_output?
File.join logdir, output_logfilename
end
end
def logfilename
options[:logfilename] or "#{@group.app_name}.log"
end
def logfile
if logdir
File.join logdir, logfilename
end
end
# this function is only used to daemonize the currently running process (Daemons.daemonize)
def start_none
unless options[:ontop]
Daemonize.daemonize(output_logfile, @group.app_name)
else
Daemonize.simulate(output_logfile)
end
@pid.pid = Process.pid
# We need this to remove the pid-file if the applications exits by itself.
# Note that at_text will only be run if the applications exits by calling
# exit, and not if it calls exit! (so please don't call exit!
# in your application!
#
at_exit do
begin; @pid.cleanup; rescue ::Exception; end
# If the option :backtrace is used and the application did exit by itself
# create a exception log.
if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
begin; exception_log; rescue ::Exception; end
end
end
# This part is needed to remove the pid-file if the application is killed by
# daemons or manually by the user.
# Note that the applications is not supposed to overwrite the signal handler for
# 'TERM'.
#
trap(SIGNAL) do
begin; @pid.cleanup; rescue ::Exception; end
$daemons_sigterm = true
if options[:hard_exit]
exit!
else
exit
end
end
end
def start_exec
if options[:backtrace]
@report.backtrace_not_supported
end
unless options[:ontop]
Daemonize.daemonize(output_logfile, @group.app_name)
else
Daemonize.simulate(output_logfile)
end
# note that we cannot remove the pid file if we run in :ontop mode (i.e. 'ruby ctrl_exec.rb run')
@pid.pid = Process.pid
ENV['DAEMONS_ARGV'] = @controller_argv.join(' ')
started
Kernel.exec(script, *(@app_argv || []))
end
def start_load
unless options[:ontop]
Daemonize.daemonize(output_logfile, @group.app_name)
else
Daemonize.simulate(output_logfile)
end
@pid.pid = Process.pid
# We need this to remove the pid-file if the applications exits by itself.
# Note that at_exit will only be run if the applications exits by calling
# exit, and not if it calls exit! (so please don't call exit!
# in your application!
#
at_exit do
begin; @pid.cleanup; rescue ::Exception; end
# If the option :backtrace is used and the application did exit by itself
# create a exception log.
if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
begin; exception_log; rescue ::Exception; end
end
end
# This part is needed to remove the pid-file if the application is killed by
# daemons or manually by the user.
# Note that the applications is not supposed to overwrite the signal handler for
# 'TERM'.
#
$daemons_stop_proc = options[:stop_proc]
trap(SIGNAL) do
begin
if $daemons_stop_proc
$daemons_stop_proc.call
end
rescue ::Exception
end
begin; @pid.cleanup; rescue ::Exception; end
$daemons_sigterm = true
if options[:hard_exit]
exit!
else
exit
end
end
# Now we really start the script...
$DAEMONS_ARGV = @controller_argv
ENV['DAEMONS_ARGV'] = @controller_argv.join(' ')
ARGV.clear
ARGV.concat @app_argv if @app_argv
started
# TODO: exception logging
load script
end
def start_proc
return unless p = options[:proc]
myproc = proc do
# We need this to remove the pid-file if the applications exits by itself.
# Note that at_text will only be run if the applications exits by calling
# exit, and not if it calls exit! (so please don't call exit!
# in your application!
#
at_exit do
begin; @pid.cleanup; rescue ::Exception; end
# If the option :backtrace is used and the application did exit by itself
# create a exception log.
if options[:backtrace] && !options[:ontop] && !$daemons_sigterm
begin; exception_log; rescue ::Exception; end
end
end
# This part is needed to remove the pid-file if the application is killed by
# daemons or manually by the user.
# Note that the applications is not supposed to overwrite the signal handler for
# 'TERM'.
#
$daemons_stop_proc = options[:stop_proc]
trap(SIGNAL) do
begin
if $daemons_stop_proc
$daemons_stop_proc.call
end
rescue ::Exception
end
begin; @pid.cleanup; rescue ::Exception; end
$daemons_sigterm = true
if options[:hard_exit]
exit!
else
exit
end
end
p.call
end
unless options[:ontop]
@pid.pid = Daemonize.call_as_daemon(myproc, output_logfile, @group.app_name)
else
Daemonize.simulate(output_logfile)
myproc.call
end
started
end
def start(restart = false)
change_privilege
unless restart
@group.create_monitor(self) unless options[:ontop] # we don't monitor applications in the foreground
end
case options[:mode]
when :none
# this is only used to daemonize the currently running process
start_none
when :exec
start_exec
when :load
start_load
when :proc
start_proc
else
start_load
end
end
def started
if pid = @pid.pid
@report.process_started(group.app_name, pid)
end
end
def reload
if @pid.pid == 0
zap
start
else
begin
Process.kill('HUP', @pid.pid)
rescue
# ignore
end
end
end
# This is a nice little function for debugging purposes:
# In case a multi-threaded ruby script exits due to an uncaught exception
# it may be difficult to find out where the exception came from because
# one cannot catch exceptions that are thrown in threads other than the main
# thread.
#
# This function searches for all exceptions in memory and outputs them to $stderr
# (if it is connected) and to a log file in the pid-file directory.
#
def exception_log
return unless logfile
require 'logger'
l_file = Logger.new(logfile)
# the code below finds the last exception
e = nil
ObjectSpace.each_object do |o|
if ::Exception === o
e = o
end
end
l_file.info '*** below you find the most recent exception thrown, this will be likely (but not certainly) the exception that made the application exit abnormally ***'
l_file.error e
l_file.info '*** below you find all exception objects found in memory, some of them may have been thrown in your application, others may just be in memory because they are standard exceptions ***'
# this code logs every exception found in memory
ObjectSpace.each_object do |o|
if ::Exception === o
l_file.error o
end
end
l_file.close
end
def stop(no_wait = false)
unless running?
zap
return
end
# confusing: pid is also a attribute_reader
pid = @pid.pid
# Catch errors when trying to kill a process that doesn't
# exist. This happens when the process quits and hasn't been
# restarted by the monitor yet. By catching the error, we allow the
# pid file clean-up to occur.
begin
wait_and_retry_kill_harder(pid, @signals_and_waits, no_wait)
rescue Errno::ESRCH => e
@report.output_message("#{e} #{pid}")
@report.output_message('deleting pid-file.')
end
sleep(0.1)
unless Pid.running?(pid)
# We try to remove the pid-files by ourselves, in case the application
# didn't clean it up.
zap!
@report.stopped_process(group.app_name, pid)
end
end
# @param Hash remaing_signals
# @param Boolean no_wait Send first Signal and return
def wait_and_retry_kill_harder(pid, remaining_signals, no_wait = false)
sig_wait = remaining_signals.shift
sig = sig_wait[:sig]
wait = sig_wait[:wait]
Process.kill(sig, pid)
return if no_wait || !wait.positive?
@report.stopping_process(group.app_name, pid, sig, wait)
begin
Timeout.timeout(wait, TimeoutError) do
sleep(0.2) while Pid.running?(pid)
end
rescue TimeoutError
if remaining_signals.any?
wait_and_retry_kill_harder(pid, remaining_signals)
else
@report.cannot_stop_process(group.app_name, pid)
end
end
end
def zap
@pid.zap
end
def zap!
begin; @pid.zap; rescue ::Exception; end
end
def show_status
@show_status_callback.call(self)
end
def default_show_status(daemon = self)
running = daemon.running?
@report.status(group.app_name, running, daemon.pid.exist?, daemon.pid.pid.to_s)
end
# This function implements a (probably too simle) method to detect
# whether the program with the pid found in the pid-file is still running.
# It just searches for the pid in the output of ps ax, which
# is probably not a good idea in some cases.
# Alternatives would be to use a direct access method the unix process control
# system.
#
def running?
@pid.exist? and Pid.running? @pid.pid
end
private
def log_output?
options[:log_output] && logdir
end
def log_output_syslog?
options[:log_output_syslog]
end
def dir_mode
@dir_mode or group.dir_mode
end
def dir
@dir or group.dir
end
def parse_signals_and_waits(argv)
unless argv
return [
{ sig: 'TERM', wait: @force_kill_waittime },
{ sig: 'KILL', wait: 20 }
]
end
argv.split('|').collect{ |part| splitted = part.split(':'); {sig: splitted[0], wait: splitted[1].to_i}}
end
end
end