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