Module: Daemonz

Defined in:
lib/daemonz/railtie.rb,
lib/daemonz.rb,
lib/daemonz/config.rb,
lib/daemonz/killer.rb,
lib/daemonz/manage.rb,
lib/daemonz/master.rb,
lib/daemonz/logging.rb,
lib/daemonz/process.rb,
lib/daemonz/generators/config/config_generator.rb,
lib/daemonz/generators/daemon/daemon_generator.rb

Overview

:nodoc: namespace

Defined Under Namespace

Modules: Generators, ProcTable Classes: Railtie

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configObject (readonly)

Returns the value of attribute config.



6
7
8
# File 'lib/daemonz/config.rb', line 6

def config
  @config
end

.daemonsObject (readonly)

Returns the value of attribute daemons.



86
87
88
# File 'lib/daemonz/config.rb', line 86

def daemons
  @daemons
end

.keep_daemons_at_exitObject

Set by the rake tasks.



9
10
11
# File 'lib/daemonz/config.rb', line 9

def keep_daemons_at_exit
  @keep_daemons_at_exit
end

Class Method Details

.claim_masterObject

attempts to claim the master lock



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 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 = {}) ⇒ Object

figure out the plugin’s configuration



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 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_daemonsObject

process the daemon configuration



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# 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_loggerObject



8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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?Boolean

compute whether daemonz should be enabled or not

Returns:

  • (Boolean)


13
14
15
16
# 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!Object



18
19
20
21
22
23
24
# 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_lockObject



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 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 = {}) ⇒ Object

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


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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) ⇒ Object

load and parse the config file



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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

.loggerObject



4
5
6
# File 'lib/daemonz/logging.rb', line 4

def self.logger
  @logger || Rails.logger
end

.process_info(pid = nil) ⇒ Object

returns information about a process or all the running processes



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 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_lockObject



4
5
6
7
8
9
10
# 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 = {}) ⇒ Object

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



15
16
17
18
19
20
21
22
23
# 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 = {}) ⇒ Object

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



26
27
28
29
30
31
32
33
34
35
36
37
38
# 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) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 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!Object



40
41
42
43
44
45
46
# 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_syncObject



48
49
50
51
52
53
54
55
56
57
# 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) ⇒ Object



81
82
83
84
85
86
# 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!Object



59
60
61
# File 'lib/daemonz/manage.rb', line 59

def self.stop_daemons!
  @daemons.reverse.each { |daemon| stop_daemon! daemon }
end

.with_daemons(logger = 'rails') ⇒ Object

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



5
6
7
8
9
10
11
12
# 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