evented_bluepill 0.0.46
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/DESIGN.md +10 -0
- data/LICENSE +22 -0
- data/README.md +228 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/bin/bluepill +109 -0
- data/bin/bpsv +3 -0
- data/evented_bluepill.gemspec +86 -0
- data/lib/bluepill/application/client.rb +7 -0
- data/lib/bluepill/application/server.rb +24 -0
- data/lib/bluepill/application.rb +202 -0
- data/lib/bluepill/condition_watch.rb +55 -0
- data/lib/bluepill/controller.rb +120 -0
- data/lib/bluepill/dsl.rb +155 -0
- data/lib/bluepill/group.rb +71 -0
- data/lib/bluepill/logger.rb +62 -0
- data/lib/bluepill/process.rb +422 -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_conditions.rb +13 -0
- data/lib/bluepill/process_statistics.rb +24 -0
- data/lib/bluepill/socket.rb +47 -0
- data/lib/bluepill/system.rb +229 -0
- data/lib/bluepill/trigger.rb +70 -0
- data/lib/bluepill/triggers/flapping.rb +59 -0
- data/lib/bluepill/util/rotational_array.rb +72 -0
- data/lib/bluepill/version.rb +3 -0
- data/lib/bluepill.rb +31 -0
- data/lib/example.rb +85 -0
- data/lib/runit_example.rb +25 -0
- metadata +175 -0
@@ -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
|
data/lib/bluepill/dsl.rb
ADDED
@@ -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
|