require 'rake'
require 'rake/tasklib'
module Lifeline
##
# @private
def get_process_list
processes = %x{ps ax -o pid,command}
return nil if processes.nil?
processes.split(/\n/).map do |p|
if p =~ /^\s+(\d+)\s(.+)$/
{:pid => $1.to_i, :command => $2.strip}
end
end.compact
end
##
# A method for executing a block of code only if there is no other process with the same command running. This is useful if you want
# to have a perpetually running daemon that only executes once at a time. It uses the process name returned by ps ax to see if there is
# a process with the same command name but a different PID already executing. If so, it terminates without running the block. NOTE: since it
# uses the command name returned from ps ax, it us up to to you to name the process containing this code with a distinctly unique name. If two
# separate Rails projects both have a rake lifeline task they WILL interfere with each other. I'd suggest prefixing with the project name (ie,
# doc_viewer:lifeline) to be sure
#
# @param &block a block which is executed if there is not already a lifeline running.
# @raise [ArgumentError] if you do not pass in a block argument
def lifeline
if !block_given?
raise ArgumentError, "You must pass in a block to be the body of the run rake task"
end
my_pid = $$
processes = get_process_list
if processes.nil? || processes.empty?
raise "No processes being returned by get_process_list. Aborting!"
end
myself = processes.detect {|p| p[:pid] == my_pid}
if myself.nil?
raise "Unable to find self (PID=#{my_pid}) in process list. This is bizarre to say the least. Exiting.\n#{processes.map {|p| p.inspect}.join("\n")}"
end
# there isn't already another process running with the same command
if !processes.any? {|p| p[:pid] != my_pid && p[:command] == myself[:command]}
yield
end
end
# Define rake tasks for running, starting, and terminating
class LifelineRakeTask < ::Rake::TaskLib
# The namespace to define the tasks in
# @return [String] the namespace for the tasks
attr_accessor :namespace
##
# Creates 3 new tasks for the lifeline in the namespace specified. These
# tasks are
# * run - a task for running the code provided in the block
# * lifeline - a lifeline task for running the run task if it's not already running
# * terminate - a task for terminating all lifelines.
#
# @param [String, Symbol] name the namespace of the rake tasks
# @param [optional, Hash] opts Additional options for the method
# @option opts [Array] :prereqs ([]) If there any any rake tasks that should be prerequisites of the :run task, specify them here (For Rails, you would do :prereqs => :environment)
# @param a block that defines the body of the run task
def initialize(namespace, opts={}, &block)
if !block_given?
raise ArgumentError, "You must pass in a block to be the body of the run rake task"
end
@namespace = namespace
define_run_task(opts, &block)
define_lifeline_task
define_terminate_task
end
protected
def run_task_name
"#{namespace}:run"
end
def define_run_task(opts={}, &block)
desc "Runs the #{namespace}:run task"
task_arg = if opts[:prereqs]
{run_task_name => opts[:prereqs]}
else
run_task_name
end
task(task_arg, &block)
end
def define_lifeline_task
desc "A lifeline task for executing only one process of #{namespace}:run at a time"
task("#{namespace}:lifeline") do
lifeline do
Rake::Task["#{namespace}:run"].invoke
end
end
end
def define_terminate_task
desc "Terminates any running #{namespace}:lifeline tasks"
task("#{namespace}:terminate") do
unless (process = %x{ps aux | grep "#{namespace}:lifeline" | grep ruby | grep -v grep}.chomp).empty?
runner_pid = process.gsub(/(\s+)/, ' ').split(' ')[1]
puts %x{kill -9 #{runner_pid}}
end
end
end
end
##
# A method that defines 3 rake tasks for doing lifelines:
# * namespace:run runs the specified block
# * namespace:lifeline a lifeline for executing only a single copy of namespace:run at a time
# * namespace:terminate a task for terminating the lifelines
#
# @param [String,Symbol] namespace the namespace to define the 3 tasks in
# @param &block a block which defines the body of the namespace:run method
#
# @raise [ArgumentError] if you do not pass in a block argument
def define_lifeline_tasks(namespace, &block)
LifelineRakeTask.new(namespace, &block)
end
end