# -*- encoding: utf-8 -*- require 'thread' module Bluepill class Application PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status] attr_accessor :name, :logger, :base_dir, :socket, :pid_file, :kill_timeout attr_accessor :groups, :work_queue attr_accessor :pids_dir, :log_file def initialize(name, options = {}) self.name = name @foreground = options[:foreground] self.log_file = options[:log_file] self.base_dir = options[:base_dir] || File.join(ENV['HOME'], '.bluepill') self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid") self.pids_dir = File.join(self.base_dir, 'pids', self.name) self.kill_timeout = options[:kill_timeout] || 10 self.groups = {} self.logger = Bluepill::Logger.new(:log_file => self.log_file, :stdout => foreground?).prefix_with(self.name) self.setup_signal_traps self.setup_pids_dir @mutex = Mutex.new end def foreground? !!@foreground end def mutex(&b) @mutex.synchronize(&b) end def load begin self.start_server rescue StandardError => e $stderr.puts "Failed to start bluepill:" $stderr.puts "%s `%s`" % [e.class.name, e.message] $stderr.puts e.backtrace exit(5) end end PROCESS_COMMANDS.each do |command| class_eval <<-END def #{command}(group_name = nil, process_name = nil) self.send_to_process_or_group(:#{command}, group_name, process_name) end END end def add_process(process, group_name = nil) group_name = group_name.to_s if group_name self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name)) self.groups[group_name].add_process(process) end def version Bluepill::VERSION end protected def send_to_process_or_group(method, group_name, process_name) if group_name.nil? && process_name.nil? self.groups.values.collect do |group| group.send(method) end.flatten elsif self.groups.key?(group_name) self.groups[group_name].send(method, process_name) elsif process_name.nil? # they must be targeting just by process name process_name = group_name self.groups.values.collect do |group| group.send(method, process_name) end.flatten else [] end end def start_listener @listener_thread.kill if @listener_thread @listener_thread = Thread.new do loop do begin client = self.socket.accept client.close_on_exec = true if client.respond_to?(:close_on_exec=) command, *args = client.readline.strip.split(":") response = begin mutex { self.send(command, *args) } rescue Exception => e e end client.write(Marshal.dump(response)) rescue StandardError => e logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message]) e.backtrace.each {|l| logger.err(l)} ensure begin client.close rescue IOError # closed stream end end end end end def start_server self.kill_previous_bluepill Daemonize.daemonize unless foreground? self.logger.reopen $0 = "bluepill: #{self.name}" self.groups.each {|_, group| group.determine_initial_state } self.write_pid_file self.socket = Bluepill::Socket.server(self.base_dir, self.name) self.start_listener self.run end def run @running = true # set to false by signal trap while @running mutex do System.reset_data self.groups.each { |_, group| group.tick } end sleep 1 end cleanup end def cleanup File.unlink(self.socket.path) if self.socket File.unlink(self.pid_file) if File.exists?(self.pid_file) end def setup_signal_traps terminator = Proc.new do puts "Terminating..." @running = false end Signal.trap("TERM", &terminator) Signal.trap("INT", &terminator) Signal.trap("HUP") do self.logger.reopen if self.logger end end def setup_pids_dir FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir) # we need everybody to be able to write to the pids_dir as processes managed by # bluepill will be writing to this dir after they've dropped privileges FileUtils.chmod(0777, self.pids_dir) end def kill_previous_bluepill if File.exists?(self.pid_file) previous_pid = File.read(self.pid_file).to_i if System.pid_alive?(previous_pid) begin ::Process.kill(0, previous_pid) puts "Killing previous bluepill[#{previous_pid}]" ::Process.kill(2, previous_pid) rescue Exception => e $stderr.puts "Encountered error trying to kill previous bluepill:" $stderr.puts "#{e.class}: #{e.message}" exit(4) unless e.is_a?(Errno::ESRCH) else kill_timeout.times do |i| sleep 0.5 break unless System.pid_alive?(previous_pid) end if System.pid_alive?(previous_pid) $stderr.puts "Previous bluepill[#{previous_pid}] didn't die" exit(4) end end end end end def write_pid_file File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) } end end end