dylanvaughn-bluepill 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/DESIGN.md +10 -0
- data/LICENSE +22 -0
- data/README.md +228 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/bin/bluepill +103 -0
- data/bin/bpsv +3 -0
- data/bluepill.gemspec +84 -0
- data/lib/bluepill.rb +32 -0
- data/lib/bluepill/application.rb +200 -0
- data/lib/bluepill/application/client.rb +7 -0
- data/lib/bluepill/application/server.rb +24 -0
- data/lib/bluepill/condition_watch.rb +55 -0
- data/lib/bluepill/controller.rb +119 -0
- data/lib/bluepill/dsl.rb +150 -0
- data/lib/bluepill/group.rb +71 -0
- data/lib/bluepill/logger.rb +62 -0
- data/lib/bluepill/process.rb +419 -0
- data/lib/bluepill/process_conditions.rb +13 -0
- data/lib/bluepill/process_conditions/always_true.rb +17 -0
- data/lib/bluepill/process_conditions/cpu_usage.rb +18 -0
- data/lib/bluepill/process_conditions/http.rb +52 -0
- data/lib/bluepill/process_conditions/mem_usage.rb +31 -0
- data/lib/bluepill/process_conditions/process_condition.rb +21 -0
- data/lib/bluepill/process_statistics.rb +24 -0
- data/lib/bluepill/socket.rb +47 -0
- data/lib/bluepill/system.rb +227 -0
- data/lib/bluepill/trigger.rb +60 -0
- data/lib/bluepill/triggers/flapping.rb +59 -0
- data/lib/bluepill/util/rotational_array.rb +66 -0
- data/lib/bluepill/version.rb +3 -0
- data/lib/example.rb +81 -0
- data/lib/runit_example.rb +25 -0
- metadata +167 -0
    
        data/lib/bluepill.rb
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'thread'
         | 
| 4 | 
            +
            require 'monitor'
         | 
| 5 | 
            +
            require 'syslog'
         | 
| 6 | 
            +
            require 'timeout'
         | 
| 7 | 
            +
            require 'logger'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require 'active_support/inflector'
         | 
| 10 | 
            +
            require 'active_support/core_ext/hash'
         | 
| 11 | 
            +
            require 'active_support/core_ext/numeric'
         | 
| 12 | 
            +
            require 'active_support/core_ext/object/misc'
         | 
| 13 | 
            +
            require 'active_support/duration'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            require 'bluepill/application'
         | 
| 16 | 
            +
            require 'bluepill/controller'
         | 
| 17 | 
            +
            require 'bluepill/socket'
         | 
| 18 | 
            +
            require "bluepill/process"
         | 
| 19 | 
            +
            require "bluepill/process_statistics"
         | 
| 20 | 
            +
            require "bluepill/group"
         | 
| 21 | 
            +
            require "bluepill/logger"
         | 
| 22 | 
            +
            require "bluepill/condition_watch"
         | 
| 23 | 
            +
            require 'bluepill/trigger'
         | 
| 24 | 
            +
            require 'bluepill/triggers/flapping'
         | 
| 25 | 
            +
            require "bluepill/dsl"
         | 
| 26 | 
            +
            require "bluepill/system"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            require "bluepill/process_conditions"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            require "bluepill/util/rotational_array"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            require "bluepill/version"
         | 
| @@ -0,0 +1,200 @@ | |
| 1 | 
            +
            require 'thread'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Bluepill
         | 
| 4 | 
            +
              class Application
         | 
| 5 | 
            +
                PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status]
         | 
| 6 | 
            +
                
         | 
| 7 | 
            +
                attr_accessor :name, :logger, :base_dir, :socket, :pid_file
         | 
| 8 | 
            +
                attr_accessor :groups, :work_queue
         | 
| 9 | 
            +
                attr_accessor :pids_dir, :log_file
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(name, options = {})
         | 
| 12 | 
            +
                  self.name = name
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  @foreground   = options[:foreground]
         | 
| 15 | 
            +
                  self.log_file = options[:log_file]      
         | 
| 16 | 
            +
                  self.base_dir = options[:base_dir] || '/var/bluepill'
         | 
| 17 | 
            +
                  self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
         | 
| 18 | 
            +
                  self.pids_dir = File.join(self.base_dir, 'pids', self.name)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  self.groups = {}
         | 
| 21 | 
            +
                  
         | 
| 22 | 
            +
                  self.logger = Bluepill::Logger.new(:log_file => self.log_file, :stdout => foreground?).prefix_with(self.name)
         | 
| 23 | 
            +
                  
         | 
| 24 | 
            +
                  self.setup_signal_traps
         | 
| 25 | 
            +
                  self.setup_pids_dir
         | 
| 26 | 
            +
                  
         | 
| 27 | 
            +
                  @mutex = Mutex.new
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def foreground?
         | 
| 31 | 
            +
                  !!@foreground
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def mutex(&b)
         | 
| 35 | 
            +
                  @mutex.synchronize(&b)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def load
         | 
| 39 | 
            +
                  begin
         | 
| 40 | 
            +
                    self.start_server
         | 
| 41 | 
            +
                  rescue StandardError => e
         | 
| 42 | 
            +
                    $stderr.puts "Failed to start bluepill:"
         | 
| 43 | 
            +
                    $stderr.puts "%s `%s`" % [e.class.name, e.message]
         | 
| 44 | 
            +
                    $stderr.puts e.backtrace
         | 
| 45 | 
            +
                    exit(5)
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                PROCESS_COMMANDS.each do |command|
         | 
| 50 | 
            +
                  class_eval <<-END
         | 
| 51 | 
            +
                    def #{command}(group_name = nil, process_name = nil)
         | 
| 52 | 
            +
                      self.send_to_process_or_group(:#{command}, group_name, process_name)
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  END
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
                
         | 
| 57 | 
            +
                def add_process(process, group_name = nil)
         | 
| 58 | 
            +
                  group_name = group_name.to_s if group_name
         | 
| 59 | 
            +
                  
         | 
| 60 | 
            +
                  self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name))
         | 
| 61 | 
            +
                  self.groups[group_name].add_process(process)
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
                
         | 
| 64 | 
            +
                def version
         | 
| 65 | 
            +
                  Bluepill::VERSION
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                protected
         | 
| 69 | 
            +
                def send_to_process_or_group(method, group_name, process_name)
         | 
| 70 | 
            +
                  if group_name.nil? && process_name.nil?
         | 
| 71 | 
            +
                    self.groups.values.collect do |group|
         | 
| 72 | 
            +
                      group.send(method)
         | 
| 73 | 
            +
                    end.flatten
         | 
| 74 | 
            +
                  elsif self.groups.key?(group_name)
         | 
| 75 | 
            +
                    self.groups[group_name].send(method, process_name)
         | 
| 76 | 
            +
                  elsif process_name.nil?
         | 
| 77 | 
            +
                    # they must be targeting just by process name
         | 
| 78 | 
            +
                    process_name = group_name
         | 
| 79 | 
            +
                    self.groups.values.collect do |group|
         | 
| 80 | 
            +
                      group.send(method, process_name)
         | 
| 81 | 
            +
                    end.flatten
         | 
| 82 | 
            +
                  else
         | 
| 83 | 
            +
                    []
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def start_listener
         | 
| 88 | 
            +
                  @listener_thread.kill if @listener_thread
         | 
| 89 | 
            +
                  @listener_thread = Thread.new do
         | 
| 90 | 
            +
                    loop do
         | 
| 91 | 
            +
                      begin
         | 
| 92 | 
            +
                        client = self.socket.accept
         | 
| 93 | 
            +
                        command, *args = client.readline.strip.split(":")
         | 
| 94 | 
            +
                        response = begin
         | 
| 95 | 
            +
                          mutex { self.send(command, *args) }
         | 
| 96 | 
            +
                        rescue Exception => e
         | 
| 97 | 
            +
                          e
         | 
| 98 | 
            +
                        end
         | 
| 99 | 
            +
                        client.write(Marshal.dump(response))
         | 
| 100 | 
            +
                      rescue StandardError => e
         | 
| 101 | 
            +
                        logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message])
         | 
| 102 | 
            +
                        e.backtrace.each {|l| logger.err(l)}
         | 
| 103 | 
            +
                      ensure
         | 
| 104 | 
            +
                        begin
         | 
| 105 | 
            +
                          client.close
         | 
| 106 | 
            +
                        rescue IOError
         | 
| 107 | 
            +
                          # closed stream
         | 
| 108 | 
            +
                        end
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
                
         | 
| 114 | 
            +
                def start_server
         | 
| 115 | 
            +
                  self.kill_previous_bluepill
         | 
| 116 | 
            +
                  
         | 
| 117 | 
            +
                  Daemonize.daemonize unless foreground?
         | 
| 118 | 
            +
                  
         | 
| 119 | 
            +
                  self.logger.reopen
         | 
| 120 | 
            +
                  
         | 
| 121 | 
            +
                  $0 = "bluepilld: #{self.name}"
         | 
| 122 | 
            +
                  
         | 
| 123 | 
            +
                  self.groups.each {|_, group| group.determine_initial_state }
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  
         | 
| 126 | 
            +
                  self.write_pid_file
         | 
| 127 | 
            +
                  self.socket = Bluepill::Socket.server(self.base_dir, self.name)
         | 
| 128 | 
            +
                  self.start_listener
         | 
| 129 | 
            +
                  
         | 
| 130 | 
            +
                  self.run
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
                
         | 
| 133 | 
            +
                def run
         | 
| 134 | 
            +
                  @running = true # set to false by signal trap
         | 
| 135 | 
            +
                  while @running
         | 
| 136 | 
            +
                    mutex do
         | 
| 137 | 
            +
                      System.reset_data
         | 
| 138 | 
            +
                      self.groups.each { |_, group| group.tick }
         | 
| 139 | 
            +
                    end
         | 
| 140 | 
            +
                    sleep 1
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                  cleanup
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
                
         | 
| 145 | 
            +
                def cleanup
         | 
| 146 | 
            +
                  File.unlink(self.socket.path) if self.socket
         | 
| 147 | 
            +
                  File.unlink(self.pid_file) if File.exists?(self.pid_file)
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
                
         | 
| 150 | 
            +
                def setup_signal_traps
         | 
| 151 | 
            +
                  terminator = lambda do
         | 
| 152 | 
            +
                    puts "Terminating..."
         | 
| 153 | 
            +
                    @running = false
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
                  
         | 
| 156 | 
            +
                  Signal.trap("TERM", &terminator) 
         | 
| 157 | 
            +
                  Signal.trap("INT", &terminator) 
         | 
| 158 | 
            +
                  
         | 
| 159 | 
            +
                  Signal.trap("HUP") do
         | 
| 160 | 
            +
                    self.logger.reopen if self.logger
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                def setup_pids_dir
         | 
| 165 | 
            +
                  FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir)
         | 
| 166 | 
            +
                  # we need everybody to be able to write to the pids_dir as processes managed by
         | 
| 167 | 
            +
                  # bluepill will be writing to this dir after they've dropped privileges
         | 
| 168 | 
            +
                  FileUtils.chmod(0777, self.pids_dir)
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
                
         | 
| 171 | 
            +
                def kill_previous_bluepill
         | 
| 172 | 
            +
                  if File.exists?(self.pid_file)
         | 
| 173 | 
            +
                    previous_pid = File.read(self.pid_file).to_i
         | 
| 174 | 
            +
                    begin
         | 
| 175 | 
            +
                      ::Process.kill(0, previous_pid)
         | 
| 176 | 
            +
                      puts "Killing previous bluepilld[#{previous_pid}]"
         | 
| 177 | 
            +
                      ::Process.kill(2, previous_pid)
         | 
| 178 | 
            +
                    rescue Exception => e
         | 
| 179 | 
            +
                      $stderr.puts "Encountered error trying to kill previous bluepill:"
         | 
| 180 | 
            +
                      $stderr.puts "#{e.class}: #{e.message}"
         | 
| 181 | 
            +
                      exit(4) unless e.is_a?(Errno::ESRCH)
         | 
| 182 | 
            +
                    else
         | 
| 183 | 
            +
                      10.times do |i|
         | 
| 184 | 
            +
                        sleep 0.5
         | 
| 185 | 
            +
                        break unless System.pid_alive?(previous_pid)
         | 
| 186 | 
            +
                      end
         | 
| 187 | 
            +
                      
         | 
| 188 | 
            +
                      if System.pid_alive?(previous_pid)
         | 
| 189 | 
            +
                        $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
         | 
| 190 | 
            +
                        exit(4)
         | 
| 191 | 
            +
                      end
         | 
| 192 | 
            +
                    end
         | 
| 193 | 
            +
                  end
         | 
| 194 | 
            +
                end
         | 
| 195 | 
            +
                
         | 
| 196 | 
            +
                def write_pid_file
         | 
| 197 | 
            +
                  File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
         | 
| 198 | 
            +
                end
         | 
| 199 | 
            +
              end
         | 
| 200 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module Bluepill
         | 
| 2 | 
            +
              module Application
         | 
| 3 | 
            +
                module ServerMethods
         | 
| 4 | 
            +
                  
         | 
| 5 | 
            +
                  def status
         | 
| 6 | 
            +
                    buffer = ""
         | 
| 7 | 
            +
                    self.processes.each do | process |
         | 
| 8 | 
            +
                      buffer << "#{process.name} #{process.state}\n" +
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                    buffer
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def restart
         | 
| 14 | 
            +
                    self.socket = Bluepill::Socket.new(name, base_dir).client      
         | 
| 15 | 
            +
                    socket.send("restart\n", 0)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def stop
         | 
| 19 | 
            +
                    self.socket = Bluepill::Socket.new(name, base_dir).client      
         | 
| 20 | 
            +
                    socket.send("stop\n", 0)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            module Bluepill
         | 
| 2 | 
            +
              class ConditionWatch
         | 
| 3 | 
            +
                attr_accessor :logger, :name
         | 
| 4 | 
            +
                EMPTY_ARRAY = [].freeze # no need to recreate one every tick
         | 
| 5 | 
            +
                
         | 
| 6 | 
            +
                def initialize(name, options = {})
         | 
| 7 | 
            +
                  @name = name
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  @logger = options.delete(:logger)      
         | 
| 10 | 
            +
                  @fires = options.has_key?(:fires) ? Array(options.delete(:fires)) : [:restart]
         | 
| 11 | 
            +
                  @every = options.delete(:every)
         | 
| 12 | 
            +
                  @times = options.delete(:times) || [1,1]
         | 
| 13 | 
            +
                  @times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  self.clear_history!
         | 
| 16 | 
            +
                  
         | 
| 17 | 
            +
                  @process_condition = ProcessConditions[@name].new(options)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
                
         | 
| 20 | 
            +
                def run(pid, tick_number = Time.now.to_i)
         | 
| 21 | 
            +
                  if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
         | 
| 22 | 
            +
                    @last_ran_at = tick_number
         | 
| 23 | 
            +
                    self.record_value(@process_condition.run(pid))
         | 
| 24 | 
            +
                    return @fires if self.fired?
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                  EMPTY_ARRAY
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                def record_value(value)
         | 
| 30 | 
            +
                  # TODO: record value in ProcessStatistics
         | 
| 31 | 
            +
                  @history[@history_index] = [value, @process_condition.check(value)]
         | 
| 32 | 
            +
                  @history_index = (@history_index + 1) % @history.size
         | 
| 33 | 
            +
                  self.logger.info(self.to_s)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
                
         | 
| 36 | 
            +
                def clear_history!
         | 
| 37 | 
            +
                  @last_ran_at = nil
         | 
| 38 | 
            +
                  @history = Array.new(@times[1])
         | 
| 39 | 
            +
                  @history_index = 0
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
                
         | 
| 42 | 
            +
                def fired?
         | 
| 43 | 
            +
                  @history.select {|v| v && !v[1]}.size >= @times[0]
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
                
         | 
| 46 | 
            +
                def to_s
         | 
| 47 | 
            +
                  # TODO: this will be out of order because of the way history values are assigned
         | 
| 48 | 
            +
                  # use (@history[(@history_index - 1)..1] + @history[0..(@history_index - 1)]).
         | 
| 49 | 
            +
                  #       collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
         | 
| 50 | 
            +
                  # but that's gross so... it's gonna be out of order till we figure out a better way to get it in order
         | 
| 51 | 
            +
                  data = @history.collect {|v| "#{@process_condition.format_value(v[0])}#{v[1] ? '' : '*'}" if v}.compact.join(", ")
         | 
| 52 | 
            +
                  "#{@name}: [#{data}]"
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,119 @@ | |
| 1 | 
            +
            require 'fileutils'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Bluepill
         | 
| 4 | 
            +
              class Controller
         | 
| 5 | 
            +
                attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
         | 
| 6 | 
            +
                
         | 
| 7 | 
            +
                def initialize(options = {})
         | 
| 8 | 
            +
                  self.log_file = options[:log_file]
         | 
| 9 | 
            +
                  self.base_dir = options[:base_dir]
         | 
| 10 | 
            +
                  self.sockets_dir = File.join(base_dir, 'socks')
         | 
| 11 | 
            +
                  self.pids_dir = File.join(base_dir, 'pids')
         | 
| 12 | 
            +
                  
         | 
| 13 | 
            +
                  setup_dir_structure
         | 
| 14 | 
            +
                  cleanup_bluepill_directory
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
                
         | 
| 17 | 
            +
                def running_applications
         | 
| 18 | 
            +
                  Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                def handle_command(application, command, *args)
         | 
| 22 | 
            +
                  case command.to_sym
         | 
| 23 | 
            +
                  when :status
         | 
| 24 | 
            +
                    puts self.send_to_daemon(application, :status, *args)
         | 
| 25 | 
            +
                  when *Application::PROCESS_COMMANDS
         | 
| 26 | 
            +
                    # these need to be sent to the daemon and the results printed out
         | 
| 27 | 
            +
                    affected = self.send_to_daemon(application, command, *args)
         | 
| 28 | 
            +
                    if affected.empty?
         | 
| 29 | 
            +
                      puts "No processes effected"
         | 
| 30 | 
            +
                    else
         | 
| 31 | 
            +
                      puts "Sent #{command} to:"
         | 
| 32 | 
            +
                      affected.each do |process|
         | 
| 33 | 
            +
                        puts "  #{process}"
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  when :quit
         | 
| 37 | 
            +
                    pid = pid_for(application)
         | 
| 38 | 
            +
                    if System.pid_alive?(pid)
         | 
| 39 | 
            +
                      ::Process.kill("TERM", pid)
         | 
| 40 | 
            +
                      puts "Killing bluepilld[#{pid}]"
         | 
| 41 | 
            +
                    else
         | 
| 42 | 
            +
                      puts "bluepilld[#{pid}] not running"
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  when :log
         | 
| 45 | 
            +
                    log_file_location = self.send_to_daemon(application, :log_file)
         | 
| 46 | 
            +
                    log_file_location = self.log_file if log_file_location.to_s.strip.empty?
         | 
| 47 | 
            +
                    
         | 
| 48 | 
            +
                    requested_pattern = args.first
         | 
| 49 | 
            +
                    grep_pattern = self.grep_pattern(application, requested_pattern)
         | 
| 50 | 
            +
                    
         | 
| 51 | 
            +
                    tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
         | 
| 52 | 
            +
                    puts "Tailing log for #{requested_pattern}..."
         | 
| 53 | 
            +
                    Kernel.exec(tail)
         | 
| 54 | 
            +
                  else
         | 
| 55 | 
            +
                    $stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
                
         | 
| 59 | 
            +
                def send_to_daemon(application, command, *args)
         | 
| 60 | 
            +
                  begin
         | 
| 61 | 
            +
                    verify_version!(application)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    command = ([command, *args]).join(":")
         | 
| 64 | 
            +
                    response = Socket.client_command(base_dir, application, command)
         | 
| 65 | 
            +
                    if response.is_a?(Exception)
         | 
| 66 | 
            +
                      $stderr.puts "Received error from server:"
         | 
| 67 | 
            +
                      $stderr.puts response.inspect
         | 
| 68 | 
            +
                      $stderr.puts response.backtrace.join("\n")
         | 
| 69 | 
            +
                      exit(8)
         | 
| 70 | 
            +
                    else
         | 
| 71 | 
            +
                      response
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  rescue Errno::ECONNREFUSED
         | 
| 75 | 
            +
                    abort("Connection Refused: Server is not running")
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
                
         | 
| 79 | 
            +
                def grep_pattern(application, query = nil)
         | 
| 80 | 
            +
                  pattern = [application, query].compact.join(':')
         | 
| 81 | 
            +
                  ['\[.*', Regexp.escape(pattern), '.*'].compact.join
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
                private
         | 
| 84 | 
            +
                
         | 
| 85 | 
            +
                def cleanup_bluepill_directory
         | 
| 86 | 
            +
                  self.running_applications.each do |app|
         | 
| 87 | 
            +
                    pid = pid_for(app)
         | 
| 88 | 
            +
                    if !pid || !System.pid_alive?(pid)
         | 
| 89 | 
            +
                      pid_file = File.join(self.pids_dir, "#{app}.pid")
         | 
| 90 | 
            +
                      sock_file = File.join(self.sockets_dir, "#{app}.sock")
         | 
| 91 | 
            +
                      File.unlink(pid_file) if File.exists?(pid_file)
         | 
| 92 | 
            +
                      File.unlink(sock_file) if File.exists?(sock_file)
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
                
         | 
| 97 | 
            +
                def pid_for(app)
         | 
| 98 | 
            +
                  pid_file = File.join(self.pids_dir, "#{app}.pid")
         | 
| 99 | 
            +
                  File.exists?(pid_file) && File.read(pid_file).to_i
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
                
         | 
| 102 | 
            +
                def setup_dir_structure
         | 
| 103 | 
            +
                  [@sockets_dir, @pids_dir].each do |dir|
         | 
| 104 | 
            +
                    FileUtils.mkdir_p(dir) unless File.exists?(dir)
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
                
         | 
| 108 | 
            +
                def verify_version!(application)
         | 
| 109 | 
            +
                  begin
         | 
| 110 | 
            +
                    version = Socket.client_command(base_dir, application, "version")
         | 
| 111 | 
            +
                    if version != Bluepill::VERSION
         | 
| 112 | 
            +
                      abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
                  rescue ArgumentError
         | 
| 115 | 
            +
                    abort("The running version of your daemon seems to be out of date.")
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
              end
         | 
| 119 | 
            +
            end
         | 
    
        data/lib/bluepill/dsl.rb
    ADDED
    
    | @@ -0,0 +1,150 @@ | |
| 1 | 
            +
            require 'ostruct'
         | 
| 2 | 
            +
            module Bluepill
         | 
| 3 | 
            +
              def self.define_process_condition(name, &block)
         | 
| 4 | 
            +
                klass = Class.new(ProcessConditions::ProcessCondition, &block)
         | 
| 5 | 
            +
                ProcessConditions.const_set("#{name.to_s.camelcase}", klass)
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
              
         | 
| 8 | 
            +
              def self.application(app_name, options = {}, &block)
         | 
| 9 | 
            +
                app = Application.new(app_name.to_s, options, &block)
         | 
| 10 | 
            +
               
         | 
| 11 | 
            +
                process_proxy = Class.new do
         | 
| 12 | 
            +
                  attr_reader :attributes, :watches
         | 
| 13 | 
            +
                  def initialize(process_name = nil)
         | 
| 14 | 
            +
                    @name = process_name
         | 
| 15 | 
            +
                    @attributes = {}
         | 
| 16 | 
            +
                    @watches = {}
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                  
         | 
| 19 | 
            +
                  def method_missing(name, *args)
         | 
| 20 | 
            +
                    if args.size == 1 && name.to_s =~ /^(.*)=$/
         | 
| 21 | 
            +
                      @attributes[$1.to_sym] = args.first
         | 
| 22 | 
            +
                    elsif args.empty? && @attributes.key?(name.to_sym)
         | 
| 23 | 
            +
                      @attributes[name.to_sym]
         | 
| 24 | 
            +
                    else
         | 
| 25 | 
            +
                      super
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                  
         | 
| 29 | 
            +
                  def checks(name, options = {})
         | 
| 30 | 
            +
                    @watches[name] = options
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                  
         | 
| 33 | 
            +
                  def validate_child_process(child)
         | 
| 34 | 
            +
                    unless child.attributes.has_key?(:stop_command)
         | 
| 35 | 
            +
                      $stderr.puts "Config Error: Invalid child process monitor for #{@name}"
         | 
| 36 | 
            +
                      $stderr.puts "You must specify a stop command to monitor child processes."
         | 
| 37 | 
            +
                      exit(6)
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  
         | 
| 41 | 
            +
                  def create_child_process_template
         | 
| 42 | 
            +
                    if @child_process_block
         | 
| 43 | 
            +
                      child_proxy = self.class.new
         | 
| 44 | 
            +
                      # Children inherit some properties of the parent
         | 
| 45 | 
            +
                      [:start_grace_time, :stop_grace_time, :restart_grace_time,
         | 
| 46 | 
            +
                        :start_wait_time, :stop_wait_time, :restart_wait_time].each do |attribute|
         | 
| 47 | 
            +
                        child_proxy.send("#{attribute}=", @attributes[attribute]) if @attributes.key?(attribute)
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
                      @child_process_block.call(child_proxy)
         | 
| 50 | 
            +
                      validate_child_process(child_proxy)
         | 
| 51 | 
            +
                      @attributes[:child_process_template] = child_proxy.to_process(nil)
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                  
         | 
| 55 | 
            +
                  def monitor_children(&child_process_block)
         | 
| 56 | 
            +
                    @child_process_block = child_process_block
         | 
| 57 | 
            +
                    @attributes[:monitor_children] = true
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                  
         | 
| 60 | 
            +
                  def to_process(process_name)
         | 
| 61 | 
            +
                    process = Bluepill::Process.new(process_name, @attributes)
         | 
| 62 | 
            +
                    @watches.each do |name, opts|
         | 
| 63 | 
            +
                      if Bluepill::Trigger[name]
         | 
| 64 | 
            +
                        process.add_trigger(name, opts)
         | 
| 65 | 
            +
                      else
         | 
| 66 | 
            +
                        process.add_watch(name, opts)
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    process
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
                
         | 
| 74 | 
            +
                app_proxy = Class.new do
         | 
| 75 | 
            +
                  @@app = app
         | 
| 76 | 
            +
                  @@process_proxy = process_proxy
         | 
| 77 | 
            +
                  @@process_keys = Hash.new # because I don't want to require Set just for validations
         | 
| 78 | 
            +
                  @@pid_files = Hash.new
         | 
| 79 | 
            +
                  attr_accessor :working_dir, :uid, :gid, :environment
         | 
| 80 | 
            +
                  
         | 
| 81 | 
            +
                  def validate_process(process, process_name)
         | 
| 82 | 
            +
                    # validate uniqueness of group:process
         | 
| 83 | 
            +
                    process_key = [process.attributes[:group], process_name].join(":")
         | 
| 84 | 
            +
                    if @@process_keys.key?(process_key)
         | 
| 85 | 
            +
                      $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
         | 
| 86 | 
            +
                      $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
         | 
| 87 | 
            +
                      $stderr.puts
         | 
| 88 | 
            +
                      exit(6)
         | 
| 89 | 
            +
                    else
         | 
| 90 | 
            +
                      @@process_keys[process_key] = 0
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                    
         | 
| 93 | 
            +
                    # validate required attributes
         | 
| 94 | 
            +
                    [:start_command].each do |required_attr|
         | 
| 95 | 
            +
                      if !process.attributes.key?(required_attr)
         | 
| 96 | 
            +
                        $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
         | 
| 97 | 
            +
                        exit(6)
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
                    
         | 
| 101 | 
            +
                    # validate uniqueness of pid files
         | 
| 102 | 
            +
                    pid_key = process.pid_file.strip
         | 
| 103 | 
            +
                    if @@pid_files.key?(pid_key)
         | 
| 104 | 
            +
                      $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
         | 
| 105 | 
            +
                      exit(6)
         | 
| 106 | 
            +
                    else
         | 
| 107 | 
            +
                      @@pid_files[pid_key] = 0
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                  
         | 
| 111 | 
            +
                  def process(process_name, &process_block)
         | 
| 112 | 
            +
                    process_proxy = @@process_proxy.new(process_name)
         | 
| 113 | 
            +
                    process_block.call(process_proxy)
         | 
| 114 | 
            +
                    process_proxy.create_child_process_template
         | 
| 115 | 
            +
                    
         | 
| 116 | 
            +
                    set_app_wide_attributes(process_proxy)
         | 
| 117 | 
            +
                    
         | 
| 118 | 
            +
                    assign_default_pid_file(process_proxy, process_name)
         | 
| 119 | 
            +
                    
         | 
| 120 | 
            +
                    validate_process(process_proxy, process_name)
         | 
| 121 | 
            +
                    
         | 
| 122 | 
            +
                    group = process_proxy.attributes.delete(:group)
         | 
| 123 | 
            +
                    process = process_proxy.to_process(process_name)
         | 
| 124 | 
            +
                    
         | 
| 125 | 
            +
                    
         | 
| 126 | 
            +
                    
         | 
| 127 | 
            +
                    @@app.add_process(process, group)
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
                  
         | 
| 130 | 
            +
                  def set_app_wide_attributes(process_proxy)
         | 
| 131 | 
            +
                    [:working_dir, :uid, :gid, :environment].each do |attribute|
         | 
| 132 | 
            +
                      unless process_proxy.attributes.key?(attribute)
         | 
| 133 | 
            +
                        process_proxy.attributes[attribute] = self.send(attribute)
         | 
| 134 | 
            +
                      end
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
                  
         | 
| 138 | 
            +
                  def assign_default_pid_file(process_proxy, process_name)
         | 
| 139 | 
            +
                    unless process_proxy.attributes.key?(:pid_file)
         | 
| 140 | 
            +
                      group_name = process_proxy.attributes["group"]
         | 
| 141 | 
            +
                      default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
         | 
| 142 | 
            +
                      process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
                
         | 
| 147 | 
            +
                yield(app_proxy.new)
         | 
| 148 | 
            +
                app.load
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
            end
         |