module Daemonz
Attributes
Set by the rake tasks.
Public Class Methods
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
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
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
# 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
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
# 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
# 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
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 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
# File lib/daemonz/logging.rb, line 4 def self.logger @logger || Rails.logger end
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
# 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
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
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
# 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
# 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
# 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
# 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
# File lib/daemonz/manage.rb, line 59 def self.stop_daemons! @daemons.reverse.each { |daemon| stop_daemon! daemon } end
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