evented_bluepill 0.0.46

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,202 @@
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
+ if System.pid_alive?(previous_pid)
175
+ begin
176
+ ::Process.kill(0, previous_pid)
177
+ puts "Killing previous bluepilld[#{previous_pid}]"
178
+ ::Process.kill(2, previous_pid)
179
+ rescue Exception => e
180
+ $stderr.puts "Encountered error trying to kill previous bluepill:"
181
+ $stderr.puts "#{e.class}: #{e.message}"
182
+ exit(4) unless e.is_a?(Errno::ESRCH)
183
+ else
184
+ 10.times do |i|
185
+ sleep 0.5
186
+ break unless System.pid_alive?(previous_pid)
187
+ end
188
+
189
+ if System.pid_alive?(previous_pid)
190
+ $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
191
+ exit(4)
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def write_pid_file
199
+ File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
200
+ end
201
+ end
202
+ 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,120 @@
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
+ exit(1)
57
+ end
58
+ end
59
+
60
+ def send_to_daemon(application, command, *args)
61
+ begin
62
+ verify_version!(application)
63
+
64
+ command = ([command, *args]).join(":")
65
+ response = Socket.client_command(base_dir, application, command)
66
+ if response.is_a?(Exception)
67
+ $stderr.puts "Received error from server:"
68
+ $stderr.puts response.inspect
69
+ $stderr.puts response.backtrace.join("\n")
70
+ exit(8)
71
+ else
72
+ response
73
+ end
74
+
75
+ rescue Errno::ECONNREFUSED
76
+ abort("Connection Refused: Server is not running")
77
+ end
78
+ end
79
+
80
+ def grep_pattern(application, query = nil)
81
+ pattern = [application, query].compact.join(':')
82
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
83
+ end
84
+ private
85
+
86
+ def cleanup_bluepill_directory
87
+ self.running_applications.each do |app|
88
+ pid = pid_for(app)
89
+ if !pid || !System.pid_alive?(pid)
90
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
91
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
92
+ File.unlink(pid_file) if File.exists?(pid_file)
93
+ File.unlink(sock_file) if File.exists?(sock_file)
94
+ end
95
+ end
96
+ end
97
+
98
+ def pid_for(app)
99
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
100
+ File.exists?(pid_file) && File.read(pid_file).to_i
101
+ end
102
+
103
+ def setup_dir_structure
104
+ [@sockets_dir, @pids_dir].each do |dir|
105
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
106
+ end
107
+ end
108
+
109
+ def verify_version!(application)
110
+ begin
111
+ version = Socket.client_command(base_dir, application, "version")
112
+ if version != Bluepill::VERSION
113
+ abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
114
+ end
115
+ rescue ArgumentError
116
+ abort("The running version of your daemon seems to be out of date.")
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,155 @@
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].each do |attribute|
46
+ child_proxy.send("#{attribute}=", @attributes[attribute]) if @attributes.key?(attribute)
47
+ end
48
+ @child_process_block.call(child_proxy)
49
+ validate_child_process(child_proxy)
50
+ @attributes[:child_process_template] = child_proxy.to_process(nil)
51
+ end
52
+ end
53
+
54
+ def monitor_children(&child_process_block)
55
+ @child_process_block = child_process_block
56
+ @attributes[:monitor_children] = true
57
+ end
58
+
59
+ def to_process(process_name)
60
+ process = Bluepill::Process.new(process_name, @attributes)
61
+ @watches.each do |name, opts|
62
+ if Bluepill::Trigger[name]
63
+ process.add_trigger(name, opts)
64
+ else
65
+ process.add_watch(name, opts)
66
+ end
67
+ end
68
+
69
+ process
70
+ end
71
+ end
72
+ app_proxy = Class.new do
73
+ if RUBY_VERSION >= '1.9'
74
+ class_variable_set(:@@app, app)
75
+ class_variable_set(:@@process_proxy, process_proxy)
76
+ class_variable_set(:@@process_keys, Hash.new) # because I don't want to require Set just for validations
77
+ class_variable_set(:@@pid_files, Hash.new)
78
+ else
79
+ @@app = app
80
+ @@process_proxy = process_proxy
81
+ @@process_keys = Hash.new
82
+ @@pid_files = Hash.new
83
+ end
84
+ attr_accessor :working_dir, :uid, :gid, :environment
85
+
86
+ def validate_process(process, process_name)
87
+ # validate uniqueness of group:process
88
+ process_key = [process.attributes[:group], process_name].join(":")
89
+ if @@process_keys.key?(process_key)
90
+ $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
91
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
92
+ $stderr.puts
93
+ exit(6)
94
+ else
95
+ @@process_keys[process_key] = 0
96
+ end
97
+
98
+ # validate required attributes
99
+ [:start_command].each do |required_attr|
100
+ if !process.attributes.key?(required_attr)
101
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
102
+ exit(6)
103
+ end
104
+ end
105
+
106
+ # validate uniqueness of pid files
107
+ pid_key = process.pid_file.strip
108
+ if @@pid_files.key?(pid_key)
109
+ $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
110
+ exit(6)
111
+ else
112
+ @@pid_files[pid_key] = 0
113
+ end
114
+ end
115
+
116
+ def process(process_name, &process_block)
117
+ process_proxy = @@process_proxy.new(process_name)
118
+ process_block.call(process_proxy)
119
+ process_proxy.create_child_process_template
120
+
121
+ set_app_wide_attributes(process_proxy)
122
+
123
+ assign_default_pid_file(process_proxy, process_name)
124
+
125
+ validate_process(process_proxy, process_name)
126
+
127
+ group = process_proxy.attributes.delete(:group)
128
+ process = process_proxy.to_process(process_name)
129
+
130
+
131
+
132
+ @@app.add_process(process, group)
133
+ end
134
+
135
+ def set_app_wide_attributes(process_proxy)
136
+ [:working_dir, :uid, :gid, :environment].each do |attribute|
137
+ unless process_proxy.attributes.key?(attribute)
138
+ process_proxy.attributes[attribute] = self.send(attribute)
139
+ end
140
+ end
141
+ end
142
+
143
+ def assign_default_pid_file(process_proxy, process_name)
144
+ unless process_proxy.attributes.key?(:pid_file)
145
+ group_name = process_proxy.attributes["group"]
146
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
147
+ process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
148
+ end
149
+ end
150
+ end
151
+
152
+ yield(app_proxy.new)
153
+ app.load
154
+ end
155
+ end
@@ -0,0 +1,71 @@
1
+ module Bluepill
2
+ class Group
3
+ attr_accessor :name, :processes, :logger
4
+ attr_accessor :process_logger
5
+
6
+ def initialize(name, options = {})
7
+ self.name = name
8
+ self.processes = []
9
+ self.logger = options[:logger]
10
+ end
11
+
12
+ def add_process(process)
13
+ process.logger = self.logger.prefix_with(process.name)
14
+ self.processes << process
15
+ end
16
+
17
+ def tick
18
+ self.processes.each do |process|
19
+ process.tick
20
+ end
21
+ end
22
+
23
+ def determine_initial_state
24
+ self.processes.each do |process|
25
+ process.determine_initial_state
26
+ end
27
+ end
28
+
29
+ # proxied events
30
+ [:start, :unmonitor, :stop, :restart].each do |event|
31
+ class_eval <<-END
32
+ def #{event}(process_name = nil)
33
+ threads = []
34
+ affected = []
35
+ self.processes.each do |process|
36
+ next if process_name && process_name != process.name
37
+ affected << [self.name, process.name].join(":")
38
+ threads << Thread.new { process.handle_user_command("#{event}") }
39
+ end
40
+ threads.each { |t| t.join }
41
+ affected
42
+ end
43
+ END
44
+ end
45
+
46
+ def status(process_name = nil)
47
+ lines = []
48
+ if process_name.nil?
49
+ prefix = self.name ? " " : ""
50
+ lines << "#{self.name}:" if self.name
51
+
52
+ self.processes.each do |process|
53
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
54
+ if process.monitor_children?
55
+ process.children.each do |child|
56
+ lines << " %s%s: %s" % [prefix, child.name, child.state]
57
+ end
58
+ end
59
+ end
60
+ else
61
+ self.processes.each do |process|
62
+ next if process_name != process.name
63
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
64
+ lines << process.statistics.to_s
65
+ end
66
+ end
67
+ lines << ""
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,62 @@
1
+ module Bluepill
2
+ class Logger
3
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @logger = options[:logger] || self.create_logger
8
+ @prefix = options[:prefix]
9
+ @stdout = options[:stdout]
10
+ @prefixes = {}
11
+ end
12
+
13
+ LOG_METHODS.each do |method|
14
+ eval <<-END
15
+ def #{method}(msg, prefix = [])
16
+ if @logger.is_a?(self.class)
17
+ @logger.#{method}(msg, [@prefix] + prefix)
18
+ else
19
+ s_prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
20
+ if @stdout
21
+ $stdout.puts("[#{method}]: \#{s_prefix}\#{msg}")
22
+ $stdout.flush
23
+ end
24
+ @logger.#{method}("\#{s_prefix}\#{msg}")
25
+ end
26
+ end
27
+ END
28
+ end
29
+
30
+ def prefix_with(prefix)
31
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
32
+ end
33
+
34
+ def reopen
35
+ if @logger.is_a?(self.class)
36
+ @logger.reopen
37
+ else
38
+ @logger = create_logger
39
+ end
40
+ end
41
+
42
+ protected
43
+ def create_logger
44
+ if @options[:log_file]
45
+ LoggerAdapter.new(@options[:log_file])
46
+ else
47
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
48
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
49
+ end
50
+ end
51
+
52
+ class LoggerAdapter < ::Logger
53
+ LOGGER_EQUIVALENTS =
54
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
55
+
56
+ LOG_METHODS.each do |method|
57
+ next if method == LOGGER_EQUIVALENTS[method]
58
+ alias_method method, LOGGER_EQUIVALENTS[method]
59
+ end
60
+ end
61
+ end
62
+ end