module Daemonz

Attributes

config[R]
daemons[R]
keep_daemons_at_exit[RW]

Set by the rake tasks.

Public Class Methods

claim_master() click to toggle source

attempts to claim the master lock

# File lib/daemonz/master.rb, line 41
def self.claim_master
  begin
    # try to grab that lock
    master_pid = grab_master_lock
    if master_pid
      logger.info "Daemonz in slave mode; PID #{master_pid} has master lock"
      return false
    else
      logger.info "Daemonz grabbed master lock"
      return true
    end
  rescue Exception => e
    logger.warn "Daemonz mastering failed: #{e.class.name} - #{e}"
    return false
  end
end
configure(config_file, options = {}) click to toggle source

figure out the plugin’s configuration

# File lib/daemonz/config.rb, line 27
def self.configure(config_file, options = {})
  load_configuration config_file

  config[:root_path] ||= Rails.root
  if options[:force_enabled]
    config[:disabled] = false
    config[:disabled_for] = []
    config[:disabled_in] = []
  else
    config[:disabled] ||= false
    config[:disabled_for] ||= ['rake', 'script/generate']
    config[:disabled_in] ||= ['test']
  end
  config[:disabled] = false if config[:disabled] == 'false'
  config[:master_file] ||= Rails.root.join "tmp", "pids", "daemonz.master.pid"

  config[:logger] &&= options[:override_logger]
  self.configure_logger

  if self.disabled?
    config[:is_master] = false
  else
    config[:is_master] = Daemonz.claim_master
  end
end
configure_daemons() click to toggle source

process the daemon configuration

# File lib/daemonz/config.rb, line 90
def self.configure_daemons
  @daemons = []
  config[:daemons].each do |name, daemon_config|
    next if daemon_config[:disabled]
    daemon = { :name => name }

    # compute the daemon startup / stop commands
    ['start', 'stop'].each do |command|
      daemon_binary = daemon_config[:binary] || daemon_config["#{command}_binary".to_sym]
      if daemon_config[:absolute_binary]
        daemon_path = `which #{daemon_binary}`.strip
        unless daemon_config[:kill_patterns]
          logger.error "Daemonz ignoring #{name}; using an absolute binary path but no custom process kill patterns"
          break
        end
      else
        daemon_path = File.join config[:root_path], daemon_binary || ''
      end
      unless daemon_binary and File.exists? daemon_path
        logger.error "Daemonz ignoring #{name}; the #{command} file is missing"
        break
      end

      unless daemon_config[:absolute_binary]
        begin
          binary_perms = File.stat(daemon_path).mode
          if binary_perms != (binary_perms | 0111)
            File.chmod(binary_perms | 0111, daemon_path)
          end
        rescue Exception => e
          # chmod might fail due to lack of permissions
          logger.error "Daemonz failed to make #{name} binary executable - #{e.class.name}: #{e}\n"
          logger.info e.backtrace.join("\n") + "\n"
        end
      end

      daemon_args = daemon_config[:args] || daemon_config["#{command}_args".to_sym]
      daemon_cmdline = "#{daemon_path} #{daemon_args}"
      daemon[command.to_sym] = {:path => daemon_path, :cmdline => daemon_cmdline}
    end
    next unless daemon[:stop]

    # kill patterns
    daemon[:kill_patterns] = daemon_config[:kill_patterns] || [daemon[:start][:path]]

    # pass-through params
    daemon[:pids] = daemon_config[:pids]
    unless daemon[:pids]
      logger.error "Daemonz ignoring #{name}; no pid file pattern specified"
      next
    end
    daemon[:delay_before_kill] = daemon_config[:delay_before_kill] || 0.2
    daemon[:start_order] = daemon_config[:start_order]

    @daemons << daemon
  end

  # sort by start_order, then by name
  @daemons.sort! do |a, b|
    if a[:start_order]
      if b[:start_order]
        if a[:start_order] != b[:start_order]
          next a[:start_order] <=> b[:start_order]
        else
          next a[:name] <=> b[:name]
        end
      else
        next 1
      end
    else
      next a[:name] <=> b[:name]
    end
  end
end
configure_logger() click to toggle source
# File lib/daemonz/logging.rb, line 8
def self.configure_logger
  case config[:logger]
  when 'stdout'
    @logger = Logger.new(STDOUT)
    @logger.level = Logger::DEBUG
  when 'stderr'
    @logger = Logger.new(STDERR)
    @logger.level = Logger::DEBUG
  when 'rails'
    @logger = Rails.logger
  else
    @logger = Rails.logger
  end
end
disabled?() click to toggle source

compute whether daemonz should be enabled or not

# File lib/daemonz/config.rb, line 13
def self.disabled?
  return config[:cached_disabled] if config.has_key? :cached_disabled
  config[:cached_disabled] = disabled_without_cache!
end
disabled_without_cache!() click to toggle source
# File lib/daemonz/config.rb, line 18
def self.disabled_without_cache!
  return true if config[:disabled]
  return true if config[:disabled_in].include? Rails.env.to_s
  config[:disabled_for].any? do |suffix|
    suffix == $0[-suffix.length, suffix.length]
  end
end
grab_master_lock() click to toggle source
# File lib/daemonz/master.rb, line 12
def self.grab_master_lock
  loop do
    File.open(config[:master_file], File::CREAT | File::RDWR) do |f|
      if f.flock File::LOCK_EX
        lock_data = f.read
        lock_data = lock_data[lock_data.index(/\d/), lock_data.length] if lock_data.index /\d/
        master = lock_data.split("\n", 2)

        if master.length == 2
          master_pid = master[0].to_i
          master_cmdline = master[1]
          if master_pid != 0
            master_pinfo = process_info(master_pid)
            return master_pid if master_pinfo and master_pinfo[:cmdline] == master_cmdline

            logger.info "Old master (PID #{master_pid}) died; breaking master lock"
          end
        end

        f.truncate 0
        f.write "#{$PID}\n#{process_info($PID)[:cmdline]}"
        f.flush
        return nil
      end
    end
  end
end
kill_process_set(kill_script, pid_patterns, process_patterns, options = {}) click to toggle source

Complex procedure for killing a process or a bunch of process replicas kill_command is the script that’s supposed to kill the process / processes (tried first) pid_patters are globs identifying PID files (a file can match any of the patterns) process_patterns are strings that should show on a command line (a process must match all) options:

:verbose - log what gets killed
:script_delay - the amount of seconds to sleep after launching the kill script
:force_script - the kill script is executed even if there are no PID files
# File lib/daemonz/killer.rb, line 10
def self.kill_process_set(kill_script, pid_patterns, process_patterns, options = {})
  # Phase 1: kill order (only if there's a PID file)
  pid_patterns = [pid_patterns] unless pid_patterns.kind_of? Enumerable
  unless options[:force_script]
    pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
  end
  if options[:force_script] or !(pid_files.empty? or kill_script.nil?)
    logger.info "Issuing kill order: #{kill_script}\n" if options[:verbose]
    unless kill_script.nil?
      child = POSIX::Spawn::Child.new kill_script
      if !child.success? and options[:verbose]
        exit_code = child.status.exitstatus
        logger.warn "Kill order failed with exit code #{exit_code}"
      end
    end

    deadline_time = Time.now + (options[:script_delay] || 0.5)
    while Time.now < deadline_time
      pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
      break if pid_files.empty?
      sleep 0.05
    end
  end

  # Phase 2: look through PID files and issue kill orders
  pinfo = process_info()
  pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
  pid_files.each do |fname|
    begin
      pid = File.open(fname, 'r') { |f| f.read.strip! }
      process_cmdline = pinfo[pid][:cmdline]
      # avoid killing innocent victims
      if pinfo[pid].nil? or process_patterns.all? { |pattern| process_cmdline.index pattern }
        logger.warn "Killing #{pid}: #{process_cmdline}" if options[:verbose]
        Process.kill 'TERM', pid.to_i
      end
    rescue
      # just in case the file gets wiped before we see it
    end
    begin
      logger.warn "Deleting #{fname}" if options[:verbose]
      File.delete fname if File.exists? fname
    rescue
      # prevents crashing if the file is wiped after we call exists?
    end
  end

  # Phase 3: look through the process table and kill anything that looks good
  pinfo = process_info()
  pinfo.each do |pid, info|
    next unless process_patterns.all? { |pattern| info[:cmdline].index pattern }
    logger.warn "Killing #{pid}: #{pinfo[pid][:cmdline]}" if options[:verbose]
    Process.kill 'TERM', pid.to_i
  end
end
load_configuration(config_file) click to toggle source

load and parse the config file

# File lib/daemonz/config.rb, line 54
def self.load_configuration(config_file)
  if File.exist? config_file
    file_contents = File.read config_file
    erb_result = ERB.new(file_contents).result
    @config = YAML.load erb_result
    @config[:daemons] ||= {}

    config_dir = File.join(File.dirname(config_file), 'daemonz')
    if File.exist? config_dir
      Dir.entries(config_dir).each do |entry|
        next unless entry =~ /^\w/  # Avoid temporary files.
        daemons_file = File.join(config_dir, entry)
        next unless File.file? daemons_file

        file_contents = File.read daemons_file
        erb_result = ERB.new(file_contents).result
        daemons = YAML.load erb_result
        daemons.keys.each do |daemon|
          if @config[:daemons].has_key? daemon
            logger.warn "Daemonz daemon file #{entry} overwrites daemon #{daemon} defined in daemonz.yml"
          end
          @config[:daemons][daemon] = daemons[daemon]
        end
      end
    end
  else
    logger.warn "Daemonz configuration not found - #{config_file}"
    @config = { :disabled => true }
  end
end
logger() click to toggle source
# File lib/daemonz/logging.rb, line 4
def self.logger
  @logger || Rails.logger
end
process_info(pid = nil) click to toggle source

returns information about a process or all the running processes

# File lib/daemonz/process.rb, line 55
def self.process_info(pid = nil)
  info = Hash.new

  Daemonz::ProcTable.ps.each do |process|
    item = { :cmdline => process.cmdline, :pid => process.pid.to_s }

    if pid.nil?
      info[process.pid.to_s] = item
    else
      return item if item[:pid].to_s == pid.to_s
    end
  end

  if pid.nil?
    return info
  else
    return nil
  end
end
release_master_lock() click to toggle source
# File lib/daemonz/master.rb, line 4
def self.release_master_lock
  if File.exist? config[:master_file]
    File.delete config[:master_file]
  else
    logger.warn "Master lock removed by someone else"
  end
end
safe_start(options = {}) click to toggle source

Complete startup used by rake:start and at Rails plug-in startup.

# File lib/daemonz/manage.rb, line 15
def self.safe_start(options = {})
  daemonz_config = Rails.root.join 'config', 'daemonz.yml'
  Daemonz.configure daemonz_config, options

  if Daemonz.config[:is_master]
    Daemonz.configure_daemons
    Daemonz.start_daemons!
  end
end
safe_stop(options = {}) click to toggle source

Complete shutdown used by rake:start and at Rails application exit.

# File lib/daemonz/manage.rb, line 26
def self.safe_stop(options = {})
  if options[:configure]
    daemonz_config = Rails.root.join 'config', 'daemonz.yml'
    Daemonz.configure daemonz_config, options
  end
  if Daemonz.config[:is_master]
    if options[:configure]
      Daemonz.configure_daemons
    end
    Daemonz.stop_daemons!
    Daemonz.release_master_lock
  end
end
start_daemon!(daemon) click to toggle source
# File lib/daemonz/manage.rb, line 63
def self.start_daemon!(daemon)
  logger.info "Daemonz killing any old instances of #{daemon[:name]}"
  # cleanup before we start
  kill_process_set daemon[:stop][:cmdline], daemon[:pids],
                   daemon[:kill_patterns],
                   :script_delay => daemon[:delay_before_kill],
                   :verbose => true, :force_script => false

  logger.info "Daemonz starting #{daemon[:name]}: #{daemon[:start][:cmdline]}"
  child = POSIX::Spawn::Child.new daemon[:start][:cmdline]

  unless child.success?
    exit_code = child.status.exitstatus
    logger.warn "Daemonz start script for #{daemon[:name]} failed " +
                "with code #{exit_code}"
  end
end
start_daemons!() click to toggle source
# File lib/daemonz/manage.rb, line 40
def self.start_daemons!
  if Daemonz.config[:async_start]
    Thread.new { start_daemons_sync }
  else
    start_daemons_sync
  end
end
start_daemons_sync() click to toggle source
# File lib/daemonz/manage.rb, line 48
def self.start_daemons_sync
  begin
    @daemons.each { |daemon| start_daemon! daemon }
  rescue Exception => e
    logger.warn "Daemonz startup process failed. #{e.class}: #{e}\n" +
                e.backtrace.join("\n")
  ensure
    logger.flush
  end
end
stop_daemon!(daemon) click to toggle source
# File lib/daemonz/manage.rb, line 81
def self.stop_daemon!(daemon)
  kill_process_set daemon[:stop][:cmdline], daemon[:pids],
                   daemon[:kill_patterns],
                   :script_delay => daemon[:delay_before_kill],
                   :verbose => true, :force_script => true
end
stop_daemons!() click to toggle source
# File lib/daemonz/manage.rb, line 59
def self.stop_daemons!
  @daemons.reverse.each { |daemon| stop_daemon! daemon }
end
with_daemons(logger = 'rails') { || ... } click to toggle source

Starts daemons, yields, stops daemons. Intended for tests.

# File lib/daemonz/manage.rb, line 5
def self.with_daemons(logger = 'rails')
  begin
    safe_start :force_enabled => true, :override_logger => logger
    yield
  ensure
    safe_stop :force_enabled => true
  end
end