|
|
@@ -0,0 +1,433 @@ |
|
|
## |
|
|
# background.rb is magic. it let's you have background rails code running |
|
|
# with |
|
|
# |
|
|
# . your app's env loaded |
|
|
# . that restarts like magic on a re-deploy |
|
|
# |
|
|
# usage in, say, ./script/run_jobs_in_the_background.rb, looks like: |
|
|
# |
|
|
# #! /usr/bin/env ruby |
|
|
# |
|
|
# Background.logger.info("process #{ Process.pid } starting...") |
|
|
# |
|
|
# logger = Background.logger |
|
|
# |
|
|
# loop do |
|
|
# begin |
|
|
# Job.each_that_needs_to_be_run do |job| |
|
|
# job.run! |
|
|
# end |
|
|
# rescue => e |
|
|
# Background.logger.error(e) |
|
|
# seconds = Rails.env == 'production' ? 42 : 2 |
|
|
# sleep(seconds) # NOTE - signals will wake this up! |
|
|
# end |
|
|
# end |
|
|
# |
|
|
# BEGIN { |
|
|
# script = __FILE__ |
|
|
# require(File.dirname(script) + '/../lib/background.rb') |
|
|
# Background.run!(script) |
|
|
# } |
|
|
# |
|
|
# it includes some basic daemon-y stuff including a lock that prevents the |
|
|
# script from running twice so cron'ing it every one minute will NOT stack |
|
|
# processes |
|
|
# |
|
|
|
|
|
module Background |
|
|
## libs |
|
|
# |
|
|
require 'fileutils' |
|
|
require 'ostruct' |
|
|
require 'rbconfig' |
|
|
|
|
|
SIGHUP = Signal.list['HUP'] |
|
|
|
|
|
%w( |
|
|
|
|
|
script |
|
|
mode |
|
|
dirname |
|
|
basename |
|
|
script_dir |
|
|
rails_root |
|
|
public_system |
|
|
private_system |
|
|
basename_dir |
|
|
lock_file |
|
|
log_file |
|
|
pid_file |
|
|
restart_txt |
|
|
started_at |
|
|
signals |
|
|
|
|
|
).each{|a| attr(a)} |
|
|
|
|
|
def run!(*args) |
|
|
setup(*args).run() |
|
|
end |
|
|
|
|
|
## setup |
|
|
# |
|
|
def setup(script) |
|
|
@script = File.expand_path(script) |
|
|
@exec = command_line |
|
|
@dirname = File.expand_path(File.dirname(@script)) |
|
|
@basename = File.basename(@script) |
|
|
@script_dir = File.expand_path(File.dirname(@script)) |
|
|
@rails_root = File.dirname(@script_dir) |
|
|
@public_system = File.expand_path(File.join(@rails_root, 'public', 'system')) |
|
|
@private_system = File.expand_path(File.join(@rails_root, 'private', 'system')) |
|
|
|
|
|
if test(?d, @private_system) |
|
|
@background_dir = File.join(@private_system, 'background') |
|
|
else |
|
|
@background_dir = File.join(@public_system, 'background') |
|
|
end |
|
|
|
|
|
@basename_dir = File.join(@background_dir, @basename) |
|
|
|
|
|
@lock_file = File.join(@basename_dir, 'lock') |
|
|
@log_file = File.join(@basename_dir, 'log') |
|
|
@pid_file = File.join(@basename_dir, 'pid') |
|
|
|
|
|
@mode = (ARGV.shift || 'RUN').upcase |
|
|
|
|
|
FileUtils.mkdir_p(@basename_dir) rescue nil |
|
|
FileUtils.touch(@lock_file) rescue nil |
|
|
FileUtils.touch(@log_file) rescue nil |
|
|
|
|
|
@signals = [] |
|
|
@restart_txt = File.join(@rails_root, 'tmp', 'restart.txt') |
|
|
@started_at = Time.now |
|
|
@monitor = new_monitor |
|
|
|
|
|
self |
|
|
end |
|
|
|
|
|
def command_line |
|
|
[which_ruby, @script, *ARGV.map{|arg| arg.dup}] |
|
|
end |
|
|
|
|
|
def new_monitor |
|
|
Thread.new(self) do |bg| |
|
|
Thread.current.abort_on_exception = true |
|
|
loop do |
|
|
#if bg.updated? |
|
|
#bg.logger.info('RESTART - update') if bg.logger |
|
|
#bg.restart! |
|
|
#end |
|
|
if bg.redeployed? |
|
|
bg.logger.info('RESTART - deploy') if bg.logger |
|
|
bg.restart! |
|
|
end |
|
|
if bg.signaled? |
|
|
bg.logger.info('RESTART - signal') if bg.logger |
|
|
bg.restart! |
|
|
end |
|
|
sleep(42) |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
def restart! |
|
|
if fork |
|
|
unlock! |
|
|
Kernel.exec(*@exec) |
|
|
else |
|
|
exit(42) |
|
|
end |
|
|
end |
|
|
|
|
|
def updated? |
|
|
a = File.stat(__FILE__).mtime rescue @started_at |
|
|
b = File.stat(@script).mtime rescue @started_at |
|
|
a > @started_at or b > @started_at |
|
|
end |
|
|
|
|
|
def redeployed? |
|
|
t = File.stat(@restart_txt).mtime rescue @started_at |
|
|
t > @started_at |
|
|
end |
|
|
|
|
|
def signaled? |
|
|
!signals.empty? |
|
|
end |
|
|
|
|
|
def which_ruby |
|
|
c = ::Config::CONFIG |
|
|
ruby = File::join(c['bindir'], c['ruby_install_name']) << c['EXEEXT'] |
|
|
raise "ruby @ #{ ruby } not executable!?" unless test(?e, ruby) |
|
|
ruby |
|
|
end |
|
|
|
|
|
# run based on mode |
|
|
# |
|
|
def run |
|
|
case @mode |
|
|
|
|
|
|
|
|
when /RUN/i |
|
|
# aquire the lock and run, otherwise exit with 42 (already running) |
|
|
# |
|
|
lock!(:complain => true) |
|
|
|
|
|
# record the pid |
|
|
# |
|
|
pid! |
|
|
|
|
|
# setup signal handling |
|
|
# |
|
|
trap! |
|
|
|
|
|
# boot the rails app |
|
|
# |
|
|
boot! |
|
|
|
|
|
# setup logging |
|
|
# |
|
|
$logger = Logging.logger(STDERR) |
|
|
|
|
|
when /RESTART/i |
|
|
# try hard to find the running process by pid and signal it to restart |
|
|
# |
|
|
pid = Integer(IO.read(@pid_file)) rescue nil |
|
|
Process.kill('HUP', pid) rescue nil if pid |
|
|
exit(0) |
|
|
|
|
|
when /START/i |
|
|
# aquire the lock and run, otherwise exit with 42 (already running) |
|
|
# |
|
|
lock!(:complain => true) |
|
|
|
|
|
# boot the rails app |
|
|
# |
|
|
boot! |
|
|
|
|
|
# daemonize |
|
|
# |
|
|
daemonize! |
|
|
|
|
|
# dump pid to file |
|
|
# |
|
|
pid! |
|
|
|
|
|
# setup signal handling |
|
|
# |
|
|
trap! |
|
|
|
|
|
# setup logging |
|
|
# |
|
|
number_rolled = 7 |
|
|
megabytes = 2 ** 20 |
|
|
max_size = 42 * megabytes |
|
|
options = { :safe => true } # for multi-process safe rolling, needs lockfile gem! |
|
|
logger = ::Logging.logger(@log_file, number_rolled, max_size, options) |
|
|
$logger = logger # HACK |
|
|
|
|
|
# redirect io to log_file |
|
|
# |
|
|
open(@log_file, 'r+') do |fd| |
|
|
fd.sync = true |
|
|
STDOUT.reopen(fd) |
|
|
STDERR.reopen(fd) |
|
|
end |
|
|
STDOUT.sync = true |
|
|
STDERR.sync = true |
|
|
|
|
|
|
|
|
when /PID/i |
|
|
# report the pid of running instance iff possible |
|
|
# |
|
|
pid = Integer(IO.read(@pid_file)) rescue nil |
|
|
if pid |
|
|
puts(pid) |
|
|
exit(0) |
|
|
else |
|
|
exit(1) |
|
|
end |
|
|
exit(1) |
|
|
|
|
|
when /FUSER/i |
|
|
# try to use fuser to display processes holding the lock |
|
|
# |
|
|
exec("fuser #{ @lock_file.inspect }") |
|
|
|
|
|
|
|
|
when /STOP/i |
|
|
# try hard to find the running process by pid and kill it |
|
|
# |
|
|
pid = Integer(IO.read(@pid_file)) rescue nil |
|
|
if pid |
|
|
10.times do |
|
|
Process.kill('TERM', pid) rescue nil |
|
|
begin |
|
|
Process.kill(0, pid) |
|
|
sleep(rand) |
|
|
rescue Errno::ESRCH |
|
|
puts(pid) |
|
|
exit(0) |
|
|
end |
|
|
end |
|
|
Process.kill(-9, pid) rescue nil |
|
|
sleep(rand) |
|
|
begin |
|
|
Process.kill(0, pid) |
|
|
rescue Errno::ESRCH |
|
|
puts(pid) |
|
|
exit(0) |
|
|
end |
|
|
end |
|
|
exit(1) |
|
|
|
|
|
when /SIGNAL/i |
|
|
# signal a running process, if any |
|
|
# |
|
|
pid = Integer(IO.read(@pid_file)) rescue nil |
|
|
if pid |
|
|
signal = ARGV.shift || 'HUP' |
|
|
Process.kill(signal, pid) |
|
|
puts(pid) |
|
|
exit(0) |
|
|
end |
|
|
exit(42) |
|
|
|
|
|
when /LOG/i |
|
|
# display the log file |
|
|
# |
|
|
puts(@log_file) |
|
|
exit(42) |
|
|
|
|
|
when /TAIL/i |
|
|
# tail the log file |
|
|
# |
|
|
system("tail -F #{ @log_file.inspect }") |
|
|
exit(42) |
|
|
|
|
|
end |
|
|
end |
|
|
|
|
|
# we'll be needing these |
|
|
# |
|
|
|
|
|
def boot! |
|
|
Dir.chdir(@rails_root) |
|
|
require File.join(@rails_root, 'config', 'boot') |
|
|
require File.join(@rails_root, 'config', 'environment') |
|
|
end |
|
|
|
|
|
def lock!(options = {}) |
|
|
complain = options['complain'] || options[:complain] |
|
|
fd = open(@lock_file, 'r+') |
|
|
status = fd.flock(File::LOCK_EX|File::LOCK_NB) |
|
|
|
|
|
unless status == 0 |
|
|
if complain |
|
|
pid = Integer(IO.read(@pid_file)) rescue '?' |
|
|
warn("instance(#{ pid }) is already running!") |
|
|
end |
|
|
exit(42) |
|
|
end |
|
|
@lock = fd # prevent garbage collection from closing the file! |
|
|
end |
|
|
|
|
|
def unlock! |
|
|
@lock.flock(File::LOCK_UN|File::LOCK_NB) if @lock |
|
|
end |
|
|
|
|
|
def pid! |
|
|
open(@pid_file, 'w+') do |fd| |
|
|
fd.puts(Process.pid) |
|
|
end |
|
|
at_exit{ FileUtils.rm_f(@pid_file) } |
|
|
end |
|
|
|
|
|
def trap! |
|
|
%w( SIGHUP SIGALRM SIGUSR1 SIGUSR2 ).each do |signal| |
|
|
trap(signal){|s| Background.signals.push(s) } |
|
|
end |
|
|
trap('SIGTERM'){ exit(0) } |
|
|
trap('SIGINT'){ exit(0) } |
|
|
end |
|
|
|
|
|
def logger |
|
|
$logger || ( |
|
|
require 'logger' unless defined?(Logger) |
|
|
Logger.new(STDERR) |
|
|
) |
|
|
end |
|
|
|
|
|
def logging_errors(&block) |
|
|
begin |
|
|
block.call() |
|
|
rescue SignalException => e |
|
|
logger.info(e) |
|
|
exit(0) |
|
|
rescue => e |
|
|
logger.error(e) |
|
|
end |
|
|
end |
|
|
|
|
|
def daemonize!(options = {}) |
|
|
# setup |
|
|
# |
|
|
chdir = options[:chdir] || options['chdir'] || '.' |
|
|
umask = options[:umask] || options['umask'] || 0 |
|
|
|
|
|
# setup pipes we'll use for IPC |
|
|
# |
|
|
ra, wa = IO.pipe |
|
|
rb, wb = IO.pipe |
|
|
|
|
|
pid = fork |
|
|
parent = !!pid |
|
|
child = !parent |
|
|
|
|
|
# in the parent we wait for the child to send back the grandchild's pid |
|
|
# |
|
|
if parent |
|
|
at_exit{ exit! } |
|
|
wa.close |
|
|
r = ra |
|
|
rb.close |
|
|
w = wb |
|
|
pid = r.gets |
|
|
w.puts(pid) |
|
|
exit(0) |
|
|
end |
|
|
|
|
|
# in the child start a grandchild. handshake the pid with the parent. |
|
|
# |
|
|
if child |
|
|
ra.close |
|
|
w = wa |
|
|
wb.close |
|
|
r = rb |
|
|
|
|
|
open("/dev/null", "r+") do |fd| |
|
|
#STDIN.reopen(fd) |
|
|
#STDOUT.reopen(fd) |
|
|
#STDERR.reopen(fd) |
|
|
end |
|
|
|
|
|
Process::setsid rescue nil |
|
|
|
|
|
pid = fork |
|
|
|
|
|
if pid |
|
|
w.puts(pid) |
|
|
r.gets |
|
|
exit! |
|
|
else |
|
|
Dir::chdir(chdir) |
|
|
File::umask(umask) |
|
|
$DAEMON = true |
|
|
at_exit{ exit! } |
|
|
end |
|
|
end |
|
|
end |
|
|
|
|
|
extend(self) |
|
|
end |