bluepill-rwgps 0.0.60
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/DESIGN.md +10 -0
- data/Gemfile +10 -0
- data/LICENSE +22 -0
- data/README.md +10 -0
- data/Rakefile +38 -0
- data/bin/bluepill +124 -0
- data/bin/bpsv +3 -0
- data/bin/sample_forking_server +53 -0
- data/bluepill-rwgps.gemspec +36 -0
- data/examples/example.rb +87 -0
- data/examples/new_example.rb +89 -0
- data/examples/new_runit_example.rb +29 -0
- data/examples/runit_example.rb +26 -0
- data/lib/bluepill/application/client.rb +8 -0
- data/lib/bluepill/application/server.rb +23 -0
- data/lib/bluepill/application.rb +205 -0
- data/lib/bluepill/condition_watch.rb +50 -0
- data/lib/bluepill/controller.rb +121 -0
- data/lib/bluepill/dsl/app_proxy.rb +25 -0
- data/lib/bluepill/dsl/process_factory.rb +122 -0
- data/lib/bluepill/dsl/process_proxy.rb +44 -0
- data/lib/bluepill/dsl.rb +12 -0
- data/lib/bluepill/group.rb +72 -0
- data/lib/bluepill/logger.rb +63 -0
- data/lib/bluepill/process.rb +490 -0
- data/lib/bluepill/process_conditions/always_true.rb +18 -0
- data/lib/bluepill/process_conditions/cpu_usage.rb +19 -0
- data/lib/bluepill/process_conditions/http.rb +58 -0
- data/lib/bluepill/process_conditions/mem_usage.rb +32 -0
- data/lib/bluepill/process_conditions/process_condition.rb +22 -0
- data/lib/bluepill/process_conditions.rb +14 -0
- data/lib/bluepill/process_statistics.rb +27 -0
- data/lib/bluepill/socket.rb +58 -0
- data/lib/bluepill/system.rb +238 -0
- data/lib/bluepill/trigger.rb +60 -0
- data/lib/bluepill/triggers/flapping.rb +56 -0
- data/lib/bluepill/util/rotational_array.rb +20 -0
- data/lib/bluepill/version.rb +4 -0
- data/lib/bluepill.rb +38 -0
- data/local-bluepill +129 -0
- data/spec/lib/bluepill/logger_spec.rb +3 -0
- data/spec/lib/bluepill/process_statistics_spec.rb +24 -0
- data/spec/lib/bluepill/system_spec.rb +36 -0
- data/spec/spec_helper.rb +19 -0
- metadata +264 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Bluepill
|
5
|
+
class Application
|
6
|
+
PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status]
|
7
|
+
|
8
|
+
attr_accessor :name, :logger, :base_dir, :socket, :pid_file, :kill_timeout
|
9
|
+
attr_accessor :groups, :work_queue
|
10
|
+
attr_accessor :pids_dir, :log_file
|
11
|
+
|
12
|
+
def initialize(name, options = {})
|
13
|
+
self.name = name
|
14
|
+
|
15
|
+
@foreground = options[:foreground]
|
16
|
+
self.log_file = options[:log_file]
|
17
|
+
self.base_dir = options[:base_dir] || '/var/run/bluepill'
|
18
|
+
self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
|
19
|
+
self.pids_dir = File.join(self.base_dir, 'pids', self.name)
|
20
|
+
self.kill_timeout = options[:kill_timeout] || 10
|
21
|
+
|
22
|
+
self.groups = {}
|
23
|
+
|
24
|
+
self.logger = Bluepill::Logger.new(:log_file => self.log_file, :stdout => foreground?).prefix_with(self.name)
|
25
|
+
|
26
|
+
self.setup_signal_traps
|
27
|
+
self.setup_pids_dir
|
28
|
+
|
29
|
+
@mutex = Mutex.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def foreground?
|
33
|
+
!!@foreground
|
34
|
+
end
|
35
|
+
|
36
|
+
def mutex(&b)
|
37
|
+
@mutex.synchronize(&b)
|
38
|
+
end
|
39
|
+
|
40
|
+
def load
|
41
|
+
begin
|
42
|
+
self.start_server
|
43
|
+
rescue StandardError => e
|
44
|
+
$stderr.puts "Failed to start bluepill:"
|
45
|
+
$stderr.puts "%s `%s`" % [e.class.name, e.message]
|
46
|
+
$stderr.puts e.backtrace
|
47
|
+
exit(5)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
PROCESS_COMMANDS.each do |command|
|
52
|
+
class_eval <<-END
|
53
|
+
def #{command}(group_name = nil, process_name = nil)
|
54
|
+
self.send_to_process_or_group(:#{command}, group_name, process_name)
|
55
|
+
end
|
56
|
+
END
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_process(process, group_name = nil)
|
60
|
+
group_name = group_name.to_s if group_name
|
61
|
+
|
62
|
+
self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name))
|
63
|
+
self.groups[group_name].add_process(process)
|
64
|
+
end
|
65
|
+
|
66
|
+
def version
|
67
|
+
Bluepill::VERSION
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
def send_to_process_or_group(method, group_name, process_name)
|
72
|
+
if group_name.nil? && process_name.nil?
|
73
|
+
self.groups.values.collect do |group|
|
74
|
+
group.send(method)
|
75
|
+
end.flatten
|
76
|
+
elsif self.groups.key?(group_name)
|
77
|
+
self.groups[group_name].send(method, process_name)
|
78
|
+
elsif process_name.nil?
|
79
|
+
# they must be targeting just by process name
|
80
|
+
process_name = group_name
|
81
|
+
self.groups.values.collect do |group|
|
82
|
+
group.send(method, process_name)
|
83
|
+
end.flatten
|
84
|
+
else
|
85
|
+
[]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def start_listener
|
90
|
+
@listener_thread.kill if @listener_thread
|
91
|
+
@listener_thread = Thread.new do
|
92
|
+
loop do
|
93
|
+
begin
|
94
|
+
client = self.socket.accept
|
95
|
+
client.close_on_exec = true if client.respond_to?(:close_on_exec=)
|
96
|
+
command, *args = client.readline.strip.split(":")
|
97
|
+
response = begin
|
98
|
+
mutex { self.send(command, *args) }
|
99
|
+
rescue Exception => e
|
100
|
+
e
|
101
|
+
end
|
102
|
+
client.write(Marshal.dump(response))
|
103
|
+
rescue StandardError => e
|
104
|
+
logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message])
|
105
|
+
e.backtrace.each {|l| logger.err(l)}
|
106
|
+
ensure
|
107
|
+
begin
|
108
|
+
client.close
|
109
|
+
rescue IOError
|
110
|
+
# closed stream
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def start_server
|
118
|
+
self.kill_previous_bluepill
|
119
|
+
|
120
|
+
Daemonize.daemonize unless foreground?
|
121
|
+
|
122
|
+
self.logger.reopen
|
123
|
+
|
124
|
+
$0 = "bluepilld: #{self.name}"
|
125
|
+
|
126
|
+
self.groups.each {|_, group| group.determine_initial_state }
|
127
|
+
|
128
|
+
|
129
|
+
self.write_pid_file
|
130
|
+
self.socket = Bluepill::Socket.server(self.base_dir, self.name)
|
131
|
+
self.start_listener
|
132
|
+
|
133
|
+
self.run
|
134
|
+
end
|
135
|
+
|
136
|
+
def run
|
137
|
+
@running = true # set to false by signal trap
|
138
|
+
while @running
|
139
|
+
mutex do
|
140
|
+
System.reset_data
|
141
|
+
self.groups.each { |_, group| group.tick }
|
142
|
+
end
|
143
|
+
sleep 1
|
144
|
+
end
|
145
|
+
cleanup
|
146
|
+
end
|
147
|
+
|
148
|
+
def cleanup
|
149
|
+
File.unlink(self.socket.path) if self.socket
|
150
|
+
File.unlink(self.pid_file) if File.exists?(self.pid_file)
|
151
|
+
end
|
152
|
+
|
153
|
+
def setup_signal_traps
|
154
|
+
terminator = Proc.new do
|
155
|
+
puts "Terminating..."
|
156
|
+
@running = false
|
157
|
+
end
|
158
|
+
|
159
|
+
Signal.trap("TERM", &terminator)
|
160
|
+
Signal.trap("INT", &terminator)
|
161
|
+
|
162
|
+
Signal.trap("HUP") do
|
163
|
+
self.logger.reopen if self.logger
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def setup_pids_dir
|
168
|
+
FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir)
|
169
|
+
# we need everybody to be able to write to the pids_dir as processes managed by
|
170
|
+
# bluepill will be writing to this dir after they've dropped privileges
|
171
|
+
FileUtils.chmod(0777, self.pids_dir)
|
172
|
+
end
|
173
|
+
|
174
|
+
def kill_previous_bluepill
|
175
|
+
if File.exists?(self.pid_file)
|
176
|
+
previous_pid = File.read(self.pid_file).to_i
|
177
|
+
if System.pid_alive?(previous_pid)
|
178
|
+
begin
|
179
|
+
::Process.kill(0, previous_pid)
|
180
|
+
puts "Killing previous bluepilld[#{previous_pid}]"
|
181
|
+
::Process.kill(2, previous_pid)
|
182
|
+
rescue Exception => e
|
183
|
+
$stderr.puts "Encountered error trying to kill previous bluepill:"
|
184
|
+
$stderr.puts "#{e.class}: #{e.message}"
|
185
|
+
exit(4) unless e.is_a?(Errno::ESRCH)
|
186
|
+
else
|
187
|
+
kill_timeout.times do |i|
|
188
|
+
sleep 0.5
|
189
|
+
break unless System.pid_alive?(previous_pid)
|
190
|
+
end
|
191
|
+
|
192
|
+
if System.pid_alive?(previous_pid)
|
193
|
+
$stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
|
194
|
+
exit(4)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def write_pid_file
|
202
|
+
File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class HistoryValue < Struct.new(:value, :critical)
|
4
|
+
end
|
5
|
+
|
6
|
+
class ConditionWatch
|
7
|
+
attr_accessor :logger, :name
|
8
|
+
EMPTY_ARRAY = [].freeze # no need to recreate one every tick
|
9
|
+
|
10
|
+
def initialize(name, options = {})
|
11
|
+
@name = name
|
12
|
+
|
13
|
+
@logger = options.delete(:logger)
|
14
|
+
@fires = options.has_key?(:fires) ? Array(options.delete(:fires)) : [:restart]
|
15
|
+
@every = options.delete(:every)
|
16
|
+
@times = options.delete(:times) || [1,1]
|
17
|
+
@times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
|
18
|
+
|
19
|
+
self.clear_history!
|
20
|
+
|
21
|
+
@process_condition = ProcessConditions[@name].new(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run(pid, tick_number = Time.now.to_i)
|
25
|
+
if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
|
26
|
+
@last_ran_at = tick_number
|
27
|
+
|
28
|
+
value = @process_condition.run(pid)
|
29
|
+
@history << HistoryValue.new(@process_condition.format_value(value), @process_condition.check(value))
|
30
|
+
self.logger.info(self.to_s)
|
31
|
+
|
32
|
+
return @fires if self.fired?
|
33
|
+
end
|
34
|
+
EMPTY_ARRAY
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear_history!
|
38
|
+
@history = Util::RotationalArray.new(@times.last)
|
39
|
+
end
|
40
|
+
|
41
|
+
def fired?
|
42
|
+
@history.count {|v| not v.critical} >= @times.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
data = @history.collect {|v| "#{v.value}#{'*' unless v.critical}"}.join(", ")
|
47
|
+
"#{@name}: [#{data}]\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Bluepill
|
5
|
+
class Controller
|
6
|
+
attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
self.log_file = options[:log_file]
|
10
|
+
self.base_dir = options[:base_dir]
|
11
|
+
self.sockets_dir = File.join(base_dir, 'socks')
|
12
|
+
self.pids_dir = File.join(base_dir, 'pids')
|
13
|
+
|
14
|
+
setup_dir_structure
|
15
|
+
cleanup_bluepill_directory
|
16
|
+
end
|
17
|
+
|
18
|
+
def running_applications
|
19
|
+
Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_command(application, command, *args)
|
23
|
+
case command.to_sym
|
24
|
+
when :status
|
25
|
+
puts self.send_to_daemon(application, :status, *args)
|
26
|
+
when *Application::PROCESS_COMMANDS
|
27
|
+
# these need to be sent to the daemon and the results printed out
|
28
|
+
affected = self.send_to_daemon(application, command, *args)
|
29
|
+
if affected.empty?
|
30
|
+
puts "No processes effected"
|
31
|
+
else
|
32
|
+
puts "Sent #{command} to:"
|
33
|
+
affected.each do |process|
|
34
|
+
puts " #{process}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
when :quit
|
38
|
+
pid = pid_for(application)
|
39
|
+
if System.pid_alive?(pid)
|
40
|
+
::Process.kill("TERM", pid)
|
41
|
+
puts "Killing bluepilld[#{pid}]"
|
42
|
+
else
|
43
|
+
puts "bluepilld[#{pid}] not running"
|
44
|
+
end
|
45
|
+
when :log
|
46
|
+
log_file_location = self.send_to_daemon(application, :log_file)
|
47
|
+
log_file_location = self.log_file if log_file_location.to_s.strip.empty?
|
48
|
+
|
49
|
+
requested_pattern = args.first
|
50
|
+
grep_pattern = self.grep_pattern(application, requested_pattern)
|
51
|
+
|
52
|
+
tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
|
53
|
+
puts "Tailing log for #{requested_pattern}..."
|
54
|
+
Kernel.exec(tail)
|
55
|
+
else
|
56
|
+
$stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
|
57
|
+
exit(1)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def send_to_daemon(application, command, *args)
|
62
|
+
begin
|
63
|
+
verify_version!(application)
|
64
|
+
|
65
|
+
command = ([command, *args]).join(":")
|
66
|
+
response = Socket.client_command(base_dir, application, command)
|
67
|
+
if response.is_a?(Exception)
|
68
|
+
$stderr.puts "Received error from server:"
|
69
|
+
$stderr.puts response.inspect
|
70
|
+
$stderr.puts response.backtrace.join("\n")
|
71
|
+
exit(8)
|
72
|
+
else
|
73
|
+
response
|
74
|
+
end
|
75
|
+
|
76
|
+
rescue Errno::ECONNREFUSED
|
77
|
+
abort("Connection Refused: Server is not running")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def grep_pattern(application, query = nil)
|
82
|
+
pattern = [application, query].compact.join(':')
|
83
|
+
['\[.*', Regexp.escape(pattern), '.*'].compact.join
|
84
|
+
end
|
85
|
+
private
|
86
|
+
|
87
|
+
def cleanup_bluepill_directory
|
88
|
+
self.running_applications.each do |app|
|
89
|
+
pid = pid_for(app)
|
90
|
+
if !pid || !System.pid_alive?(pid)
|
91
|
+
pid_file = File.join(self.pids_dir, "#{app}.pid")
|
92
|
+
sock_file = File.join(self.sockets_dir, "#{app}.sock")
|
93
|
+
File.unlink(pid_file) if File.exists?(pid_file)
|
94
|
+
File.unlink(sock_file) if File.exists?(sock_file)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def pid_for(app)
|
100
|
+
pid_file = File.join(self.pids_dir, "#{app}.pid")
|
101
|
+
File.exists?(pid_file) && File.read(pid_file).to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup_dir_structure
|
105
|
+
[@sockets_dir, @pids_dir].each do |dir|
|
106
|
+
FileUtils.mkdir_p(dir) unless File.exists?(dir)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def verify_version!(application)
|
111
|
+
begin
|
112
|
+
version = Socket.client_command(base_dir, application, "version")
|
113
|
+
if version != Bluepill::VERSION
|
114
|
+
abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
|
115
|
+
end
|
116
|
+
rescue ArgumentError
|
117
|
+
abort("The running version of your daemon seems to be out of date.")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class AppProxy
|
4
|
+
APP_ATTRIBUTES = [:working_dir, :uid, :gid, :environment, :auto_start ]
|
5
|
+
|
6
|
+
attr_accessor *APP_ATTRIBUTES
|
7
|
+
attr_reader :app
|
8
|
+
|
9
|
+
def initialize(app_name, options)
|
10
|
+
@app = Application.new(app_name.to_s, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(process_name, &process_block)
|
14
|
+
attributes = {}
|
15
|
+
APP_ATTRIBUTES.each { |a| attributes[a] = self.send(a) }
|
16
|
+
|
17
|
+
process_factory = ProcessFactory.new(attributes, process_block)
|
18
|
+
|
19
|
+
process = process_factory.create_process(process_name, @app.pids_dir)
|
20
|
+
group = process_factory.attributes.delete(:group)
|
21
|
+
|
22
|
+
@app.add_process(process, group)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class ProcessFactory
|
4
|
+
attr_reader :attributes
|
5
|
+
|
6
|
+
@@process_keys = Hash.new
|
7
|
+
@@pid_files = Hash.new
|
8
|
+
|
9
|
+
def initialize(attributes, process_block)
|
10
|
+
@attributes = attributes
|
11
|
+
@process_block = process_block
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_process(name, pids_dir)
|
15
|
+
self.assign_default_pid_file(name, pids_dir)
|
16
|
+
|
17
|
+
process = ProcessProxy.new(name, @attributes, @process_block)
|
18
|
+
child_process_block = @attributes.delete(:child_process_block)
|
19
|
+
@attributes[:child_process_factory] = ProcessFactory.new(@attributes, child_process_block) if @attributes[:monitor_children]
|
20
|
+
|
21
|
+
self.validate_process! process
|
22
|
+
process.to_process
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_child_process(name, pid, logger)
|
26
|
+
attributes = {}
|
27
|
+
[:start_grace_time, :stop_grace_time, :restart_grace_time].each {|a| attributes[a] = @attributes[a]}
|
28
|
+
attributes[:actual_pid] = pid
|
29
|
+
attributes[:logger] = logger
|
30
|
+
|
31
|
+
child = ProcessProxy.new(name, attributes, @process_block)
|
32
|
+
self.validate_child_process! child
|
33
|
+
process = child.to_process
|
34
|
+
|
35
|
+
process.determine_initial_state
|
36
|
+
process
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def assign_default_pid_file(process_name, pids_dir)
|
42
|
+
unless @attributes.key?(:pid_file)
|
43
|
+
group_name = @attributes[:group]
|
44
|
+
default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
|
45
|
+
@attributes[:pid_file] = File.join(pids_dir, default_pid_name + ".pid")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_process!(process)
|
50
|
+
# validate uniqueness of group:process
|
51
|
+
process_key = [process.attributes[:group], process.name].join(":")
|
52
|
+
if @@process_keys.key?(process_key)
|
53
|
+
$stderr.print "Config Error: You have two entries for the process name '#{process.name}'"
|
54
|
+
$stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
|
55
|
+
$stderr.puts
|
56
|
+
exit(6)
|
57
|
+
else
|
58
|
+
@@process_keys[process_key] = 0
|
59
|
+
end
|
60
|
+
|
61
|
+
# validate required attributes
|
62
|
+
[:start_command].each do |required_attr|
|
63
|
+
if !process.attributes.key?(required_attr)
|
64
|
+
$stderr.puts "Config Error: You must specify a #{required_attr} for '#{process.name}'"
|
65
|
+
exit(6)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# validate uniqueness of pid files
|
70
|
+
pid_key = process.attributes[:pid_file].strip
|
71
|
+
if @@pid_files.key?(pid_key)
|
72
|
+
$stderr.puts "Config Error: You have two entries with the pid file: #{pid_key}"
|
73
|
+
exit(6)
|
74
|
+
else
|
75
|
+
@@pid_files[pid_key] = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
#validate stop_signals array
|
79
|
+
stop_grace_time = process.attributes[:stop_grace_time]
|
80
|
+
stop_signals = process.attributes[:stop_signals]
|
81
|
+
|
82
|
+
unless stop_signals.nil?
|
83
|
+
#Start with the more helpful error messages before the 'odd number' message.
|
84
|
+
delay_sum = 0
|
85
|
+
stop_signals.each_with_index do |s_or_d, i|
|
86
|
+
if i % 2 == 0
|
87
|
+
signal = s_or_d
|
88
|
+
unless signal.is_a? Symbol
|
89
|
+
$stderr.puts "Config Error: Invalid stop_signals! Expected a symbol (signal) at position #{i} instead of '#{signal}'."
|
90
|
+
exit(6)
|
91
|
+
end
|
92
|
+
else
|
93
|
+
delay = s_or_d
|
94
|
+
unless delay.is_a? Fixnum
|
95
|
+
$stderr.puts "Config Error: Invalid stop_signals! Expected a number (delay) at position #{i} instead of '#{delay}'."
|
96
|
+
exit(6)
|
97
|
+
end
|
98
|
+
delay_sum += delay
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
unless stop_signals.size % 2 == 1
|
103
|
+
$stderr.puts "Config Error: Invalid stop_signals! Expected an odd number of elements."
|
104
|
+
exit(6)
|
105
|
+
end
|
106
|
+
|
107
|
+
if stop_grace_time.nil? || stop_grace_time <= delay_sum
|
108
|
+
$stderr.puts "Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!"
|
109
|
+
exit(6)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_child_process!(child)
|
115
|
+
unless child.attributes.has_key?(:stop_command)
|
116
|
+
$stderr.puts "Config Error: Invalid child process monitor for #{child.name}"
|
117
|
+
$stderr.puts "You must specify a stop command to monitor child processes."
|
118
|
+
exit(6)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class ProcessProxy
|
4
|
+
attr_reader :attributes, :watches, :name
|
5
|
+
def initialize(process_name, attributes, process_block)
|
6
|
+
@name = process_name
|
7
|
+
@attributes = attributes
|
8
|
+
@watches = {}
|
9
|
+
|
10
|
+
if process_block.arity == 0
|
11
|
+
instance_eval &process_block
|
12
|
+
else
|
13
|
+
instance_exec(self, &process_block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(name, *args)
|
18
|
+
if args.size == 1 && name.to_s =~ /^(.*)=$/
|
19
|
+
@attributes[$1.to_sym] = args.first
|
20
|
+
elsif args.size == 1
|
21
|
+
@attributes[name.to_sym] = args.first
|
22
|
+
elsif args.size == 0 && name.to_s =~ /^(.*)!$/
|
23
|
+
@attributes[$1.to_sym] = true
|
24
|
+
elsif args.empty? && @attributes.key?(name.to_sym)
|
25
|
+
@attributes[name.to_sym]
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def checks(name, options = {})
|
32
|
+
@watches[name] = options
|
33
|
+
end
|
34
|
+
|
35
|
+
def monitor_children(&child_process_block)
|
36
|
+
@attributes[:monitor_children] = true
|
37
|
+
@attributes[:child_process_block] = child_process_block
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_process
|
41
|
+
Process.new(@name, @watches, @attributes)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/bluepill/dsl.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
def self.application(app_name, options = {}, &block)
|
4
|
+
app_proxy = AppProxy.new(app_name, options)
|
5
|
+
if block.arity == 0
|
6
|
+
app_proxy.instance_eval &block
|
7
|
+
else
|
8
|
+
app_proxy.instance_exec(app_proxy, &block)
|
9
|
+
end
|
10
|
+
app_proxy.app.load
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class Group
|
4
|
+
attr_accessor :name, :processes, :logger
|
5
|
+
attr_accessor :process_logger
|
6
|
+
|
7
|
+
def initialize(name, options = {})
|
8
|
+
self.name = name
|
9
|
+
self.processes = []
|
10
|
+
self.logger = options[:logger]
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_process(process)
|
14
|
+
process.logger = self.logger.prefix_with(process.name)
|
15
|
+
self.processes << process
|
16
|
+
end
|
17
|
+
|
18
|
+
def tick
|
19
|
+
self.processes.each do |process|
|
20
|
+
process.tick
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def determine_initial_state
|
25
|
+
self.processes.each do |process|
|
26
|
+
process.determine_initial_state
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# proxied events
|
31
|
+
[:start, :unmonitor, :stop, :restart].each do |event|
|
32
|
+
class_eval <<-END
|
33
|
+
def #{event}(process_name = nil)
|
34
|
+
threads = []
|
35
|
+
affected = []
|
36
|
+
self.processes.each do |process|
|
37
|
+
next if process_name && process_name != process.name
|
38
|
+
affected << [self.name, process.name].join(":")
|
39
|
+
threads << Thread.new { process.handle_user_command("#{event}") }
|
40
|
+
end
|
41
|
+
threads.each { |t| t.join }
|
42
|
+
affected
|
43
|
+
end
|
44
|
+
END
|
45
|
+
end
|
46
|
+
|
47
|
+
def status(process_name = nil)
|
48
|
+
lines = []
|
49
|
+
if process_name.nil?
|
50
|
+
prefix = self.name ? " " : ""
|
51
|
+
lines << "#{self.name}:" if self.name
|
52
|
+
|
53
|
+
self.processes.each do |process|
|
54
|
+
lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
|
55
|
+
if process.monitor_children?
|
56
|
+
process.children.each do |child|
|
57
|
+
lines << " %s%s: %s" % [prefix, child.name, child.state]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
else
|
62
|
+
self.processes.each do |process|
|
63
|
+
next if process_name != process.name
|
64
|
+
lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
|
65
|
+
lines << process.statistics.to_s
|
66
|
+
end
|
67
|
+
end
|
68
|
+
lines << ""
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|