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,14 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
module ProcessConditions
|
4
|
+
def self.[](name)
|
5
|
+
const_get(name.to_s.camelcase)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require "bluepill/process_conditions/process_condition"
|
11
|
+
Dir["#{File.dirname(__FILE__)}/process_conditions/*.rb"].each do |pc|
|
12
|
+
require pc
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class ProcessStatistics
|
4
|
+
STRFTIME = "%m/%d/%Y %H:%I:%S".freeze
|
5
|
+
EVENTS_TO_PERSIST = 10
|
6
|
+
|
7
|
+
attr_reader :events
|
8
|
+
|
9
|
+
# possibly persist this data.
|
10
|
+
def initialize
|
11
|
+
@events = Util::RotationalArray.new(EVENTS_TO_PERSIST)
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_event(event, reason)
|
15
|
+
events.push([event, reason, Time.now])
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
str = events.reverse.map do |(event, reason, time)|
|
20
|
+
" #{event} at #{time.strftime(STRFTIME)} - #{reason || "unspecified"}"
|
21
|
+
end.join("\n")
|
22
|
+
|
23
|
+
"event history:\n#{str}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Bluepill
|
5
|
+
module Socket
|
6
|
+
TIMEOUT = 60 # Used for client commands
|
7
|
+
MAX_ATTEMPTS = 5
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def client(base_dir, name, &block)
|
12
|
+
UNIXSocket.open(socket_path(base_dir, name), &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def client_command(base_dir, name, command)
|
16
|
+
res = nil
|
17
|
+
MAX_ATTEMPTS.times do |current_attempt|
|
18
|
+
begin
|
19
|
+
client(base_dir, name) do |socket|
|
20
|
+
Timeout.timeout(TIMEOUT) do
|
21
|
+
socket.puts command
|
22
|
+
res = Marshal.load(socket.read)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
break
|
26
|
+
rescue EOFError, Timeout::Error
|
27
|
+
if current_attempt == MAX_ATTEMPTS - 1
|
28
|
+
abort("Socket Timeout: Server may not be responding")
|
29
|
+
end
|
30
|
+
puts "Retry #{current_attempt + 1} of #{MAX_ATTEMPTS}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
res
|
34
|
+
end
|
35
|
+
|
36
|
+
def server(base_dir, name)
|
37
|
+
socket_path = self.socket_path(base_dir, name)
|
38
|
+
begin
|
39
|
+
UNIXServer.open(socket_path)
|
40
|
+
rescue Errno::EADDRINUSE
|
41
|
+
# if sock file has been created. test to see if there is a server
|
42
|
+
begin
|
43
|
+
UNIXSocket.open(socket_path)
|
44
|
+
rescue Errno::ECONNREFUSED
|
45
|
+
File.delete(socket_path)
|
46
|
+
return UNIXServer.open(socket_path)
|
47
|
+
else
|
48
|
+
logger.err("Server is already running!")
|
49
|
+
exit(7)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def socket_path(base_dir, name)
|
55
|
+
File.join(base_dir, 'socks', name + ".sock")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'etc'
|
3
|
+
require "shellwords"
|
4
|
+
|
5
|
+
module Bluepill
|
6
|
+
# This class represents the system that bluepill is running on.. It's mainly used to memoize
|
7
|
+
# results of running ps auxx etc so that every watch in the every process will not result in a fork
|
8
|
+
module System
|
9
|
+
APPEND_MODE = "a"
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# The position of each field in ps output
|
13
|
+
IDX_MAP = {
|
14
|
+
:pid => 0,
|
15
|
+
:ppid => 1,
|
16
|
+
:pcpu => 2,
|
17
|
+
:rss => 3
|
18
|
+
}
|
19
|
+
|
20
|
+
def pid_alive?(pid)
|
21
|
+
begin
|
22
|
+
::Process.kill(0, pid)
|
23
|
+
true
|
24
|
+
rescue Errno::ESRCH
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def cpu_usage(pid)
|
30
|
+
ps_axu[pid] && ps_axu[pid][IDX_MAP[:pcpu]].to_f
|
31
|
+
end
|
32
|
+
|
33
|
+
def memory_usage(pid)
|
34
|
+
ps_axu[pid] && ps_axu[pid][IDX_MAP[:rss]].to_f
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_children(parent_pid)
|
38
|
+
child_pids = Array.new
|
39
|
+
ps_axu.each_pair do |pid, chunks|
|
40
|
+
child_pids << chunks[IDX_MAP[:pid]].to_i if chunks[IDX_MAP[:ppid]].to_i == parent_pid.to_i
|
41
|
+
end
|
42
|
+
child_pids
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the pid of the child that executes the cmd
|
46
|
+
def daemonize(cmd, options = {})
|
47
|
+
rd, wr = IO.pipe
|
48
|
+
|
49
|
+
if child = Daemonize.safefork
|
50
|
+
# we do not wanna create zombies, so detach ourselves from the child exit status
|
51
|
+
::Process.detach(child)
|
52
|
+
|
53
|
+
# parent
|
54
|
+
wr.close
|
55
|
+
|
56
|
+
daemon_id = rd.read.to_i
|
57
|
+
rd.close
|
58
|
+
|
59
|
+
return daemon_id if daemon_id > 0
|
60
|
+
|
61
|
+
else
|
62
|
+
# child
|
63
|
+
rd.close
|
64
|
+
|
65
|
+
drop_privileges(options[:uid], options[:gid], options[:supplementary_groups])
|
66
|
+
|
67
|
+
# if we cannot write the pid file as the provided user, err out
|
68
|
+
exit unless can_write_pid_file(options[:pid_file], options[:logger])
|
69
|
+
|
70
|
+
to_daemonize = lambda do
|
71
|
+
# Setting end PWD env emulates bash behavior when dealing with symlinks
|
72
|
+
Dir.chdir(ENV["PWD"] = options[:working_dir].to_s) if options[:working_dir]
|
73
|
+
options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
|
74
|
+
|
75
|
+
redirect_io(*options.values_at(:stdin, :stdout, :stderr))
|
76
|
+
|
77
|
+
::Kernel.exec(*Shellwords.shellwords(cmd))
|
78
|
+
exit
|
79
|
+
end
|
80
|
+
|
81
|
+
daemon_id = Daemonize.call_as_daemon(to_daemonize, nil, cmd)
|
82
|
+
|
83
|
+
File.open(options[:pid_file], "w") {|f| f.write(daemon_id)}
|
84
|
+
|
85
|
+
wr.write daemon_id
|
86
|
+
wr.close
|
87
|
+
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the stdout, stderr and exit code of the cmd
|
93
|
+
def execute_blocking(cmd, options = {})
|
94
|
+
rd, wr = IO.pipe
|
95
|
+
|
96
|
+
if child = Daemonize.safefork
|
97
|
+
# parent
|
98
|
+
wr.close
|
99
|
+
|
100
|
+
cmd_status = rd.read
|
101
|
+
rd.close
|
102
|
+
|
103
|
+
::Process.waitpid(child)
|
104
|
+
|
105
|
+
cmd_status.strip != '' ? Marshal.load(cmd_status) : {:exit_code => 0, :stdout => '', :stderr => ''}
|
106
|
+
else
|
107
|
+
# child
|
108
|
+
rd.close
|
109
|
+
|
110
|
+
# create a child in which we can override the stdin, stdout and stderr
|
111
|
+
cmd_out_read, cmd_out_write = IO.pipe
|
112
|
+
cmd_err_read, cmd_err_write = IO.pipe
|
113
|
+
|
114
|
+
pid = fork {
|
115
|
+
# grandchild
|
116
|
+
drop_privileges(options[:uid], options[:gid], options[:supplementary_groups])
|
117
|
+
|
118
|
+
Dir.chdir(ENV["PWD"] = options[:working_dir].to_s) if options[:working_dir]
|
119
|
+
options[:environment].each { |key, value| ENV[key.to_s] = value.to_s } if options[:environment]
|
120
|
+
|
121
|
+
# close unused fds so ancestors wont hang. This line is the only reason we are not
|
122
|
+
# using something like popen3. If this fd is not closed, the .read call on the parent
|
123
|
+
# will never return because "wr" would still be open in the "exec"-ed cmd
|
124
|
+
wr.close
|
125
|
+
|
126
|
+
# we do not care about stdin of cmd
|
127
|
+
STDIN.reopen("/dev/null")
|
128
|
+
|
129
|
+
# point stdout of cmd to somewhere we can read
|
130
|
+
cmd_out_read.close
|
131
|
+
STDOUT.reopen(cmd_out_write)
|
132
|
+
cmd_out_write.close
|
133
|
+
|
134
|
+
# same thing for stderr
|
135
|
+
cmd_err_read.close
|
136
|
+
STDERR.reopen(cmd_err_write)
|
137
|
+
cmd_err_write.close
|
138
|
+
|
139
|
+
# finally, replace grandchild with cmd
|
140
|
+
::Kernel.exec(*Shellwords.shellwords(cmd))
|
141
|
+
}
|
142
|
+
|
143
|
+
# we do not use these ends of the pipes in the child
|
144
|
+
cmd_out_write.close
|
145
|
+
cmd_err_write.close
|
146
|
+
|
147
|
+
# wait for the cmd to finish executing and acknowledge it's death
|
148
|
+
::Process.waitpid(pid)
|
149
|
+
|
150
|
+
# collect stdout, stderr and exitcode
|
151
|
+
result = {
|
152
|
+
:stdout => cmd_out_read.read,
|
153
|
+
:stderr => cmd_err_read.read,
|
154
|
+
:exit_code => $?.exitstatus
|
155
|
+
}
|
156
|
+
|
157
|
+
# We're done with these ends of the pipes as well
|
158
|
+
cmd_out_read.close
|
159
|
+
cmd_err_read.close
|
160
|
+
|
161
|
+
# Time to tell the parent about what went down
|
162
|
+
wr.write Marshal.dump(result)
|
163
|
+
wr.close
|
164
|
+
|
165
|
+
::Process.exit!
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def store
|
170
|
+
@store ||= Hash.new
|
171
|
+
end
|
172
|
+
|
173
|
+
def reset_data
|
174
|
+
store.clear unless store.empty?
|
175
|
+
end
|
176
|
+
|
177
|
+
def ps_axu
|
178
|
+
# TODO: need a mutex here
|
179
|
+
store[:ps_axu] ||= begin
|
180
|
+
# BSD style ps invocation
|
181
|
+
lines = `ps axo pid,ppid,pcpu,rss`.split("\n")
|
182
|
+
|
183
|
+
lines.inject(Hash.new) do |mem, line|
|
184
|
+
chunks = line.split(/\s+/)
|
185
|
+
chunks.delete_if {|c| c.strip.empty? }
|
186
|
+
pid = chunks[IDX_MAP[:pid]].strip.to_i
|
187
|
+
mem[pid] = chunks
|
188
|
+
mem
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# be sure to call this from a fork otherwise it will modify the attributes
|
194
|
+
# of the bluepill daemon
|
195
|
+
def drop_privileges(uid, gid, supplementary_groups)
|
196
|
+
if ::Process::Sys.geteuid == 0
|
197
|
+
uid_num = Etc.getpwnam(uid).uid if uid
|
198
|
+
gid_num = Etc.getgrnam(gid).gid if gid
|
199
|
+
|
200
|
+
supplementary_groups ||= []
|
201
|
+
|
202
|
+
group_nums = supplementary_groups.map do |group|
|
203
|
+
Etc.getgrnam(group).gid
|
204
|
+
end
|
205
|
+
|
206
|
+
::Process.groups = [gid_num] if gid
|
207
|
+
::Process.groups |= group_nums unless group_nums.empty?
|
208
|
+
::Process::Sys.setgid(gid_num) if gid
|
209
|
+
::Process::Sys.setuid(uid_num) if uid
|
210
|
+
ENV['HOME'] = Etc.getpwuid(uid_num).try(:dir) || ENV['HOME'] if uid
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def can_write_pid_file(pid_file, logger)
|
215
|
+
FileUtils.touch(pid_file)
|
216
|
+
File.unlink(pid_file)
|
217
|
+
return true
|
218
|
+
|
219
|
+
rescue Exception => e
|
220
|
+
logger.warning "%s - %s" % [e.class.name, e.message]
|
221
|
+
e.backtrace.each {|l| logger.warning l}
|
222
|
+
return false
|
223
|
+
end
|
224
|
+
|
225
|
+
def redirect_io(io_in, io_out, io_err)
|
226
|
+
$stdin.reopen(io_in) if io_in
|
227
|
+
|
228
|
+
if !io_out.nil? && !io_err.nil? && io_out == io_err
|
229
|
+
$stdout.reopen(io_out, APPEND_MODE)
|
230
|
+
$stderr.reopen($stdout)
|
231
|
+
|
232
|
+
else
|
233
|
+
$stdout.reopen(io_out, APPEND_MODE) if io_out
|
234
|
+
$stderr.reopen(io_err, APPEND_MODE) if io_err
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
class Trigger
|
4
|
+
@implementations = {}
|
5
|
+
def self.inherited(klass)
|
6
|
+
@implementations[klass.name.split('::').last.underscore.to_sym] = klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.[](name)
|
10
|
+
@implementations[name]
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :process, :logger, :mutex, :scheduled_events
|
14
|
+
|
15
|
+
def initialize(process, options = {})
|
16
|
+
self.process = process
|
17
|
+
self.logger = options[:logger]
|
18
|
+
self.mutex = Mutex.new
|
19
|
+
self.scheduled_events = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset!
|
23
|
+
self.cancel_all_events
|
24
|
+
end
|
25
|
+
|
26
|
+
def notify(transition)
|
27
|
+
raise "Implement in subclass"
|
28
|
+
end
|
29
|
+
|
30
|
+
def dispatch!(event)
|
31
|
+
self.process.dispatch!(event, self.class.name.split("::").last)
|
32
|
+
end
|
33
|
+
|
34
|
+
def schedule_event(event, delay)
|
35
|
+
# TODO: maybe wrap this in a ScheduledEvent class with methods like cancel
|
36
|
+
thread = Thread.new(self) do |trigger|
|
37
|
+
begin
|
38
|
+
sleep delay.to_f
|
39
|
+
trigger.dispatch!(event)
|
40
|
+
trigger.mutex.synchronize do
|
41
|
+
trigger.scheduled_events.delete_if { |_, thread| thread == Thread.current }
|
42
|
+
end
|
43
|
+
rescue StandardError => e
|
44
|
+
trigger.logger.err(e)
|
45
|
+
trigger.logger.err(e.backtrace.join("\n"))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
self.scheduled_events.push([event, thread])
|
50
|
+
end
|
51
|
+
|
52
|
+
def cancel_all_events
|
53
|
+
self.logger.info "Canceling all scheduled events"
|
54
|
+
self.mutex.synchronize do
|
55
|
+
self.scheduled_events.each {|_, thread| thread.kill}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
module Triggers
|
4
|
+
class Flapping < Bluepill::Trigger
|
5
|
+
TRIGGER_STATES = [:starting, :restarting]
|
6
|
+
|
7
|
+
PARAMS = [:times, :within, :retry_in]
|
8
|
+
|
9
|
+
attr_accessor *PARAMS
|
10
|
+
attr_reader :timeline
|
11
|
+
|
12
|
+
def initialize(process, options = {})
|
13
|
+
options.reverse_merge!(:times => 5, :within => 1, :retry_in => 5)
|
14
|
+
|
15
|
+
options.each_pair do |name, val|
|
16
|
+
instance_variable_set("@#{name}", val) if PARAMS.include?(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
@timeline = Util::RotationalArray.new(@times)
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def notify(transition)
|
24
|
+
if TRIGGER_STATES.include?(transition.to_name)
|
25
|
+
self.timeline << Time.now.to_i
|
26
|
+
self.check_flapping
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset!
|
31
|
+
@timeline.clear
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def check_flapping
|
36
|
+
# The process has not flapped if we haven't encountered enough incidents
|
37
|
+
return unless (@timeline.compact.length == self.times)
|
38
|
+
|
39
|
+
# Check if the incident happend within the timeframe
|
40
|
+
duration = (@timeline.last - @timeline.first) <= self.within
|
41
|
+
|
42
|
+
if duration
|
43
|
+
self.logger.info "Flapping detected: retrying in #{self.retry_in} seconds"
|
44
|
+
|
45
|
+
self.schedule_event(:start, self.retry_in) unless self.retry_in == 0 # retry_in zero means "do not retry, ever"
|
46
|
+
self.schedule_event(:unmonitor, 0)
|
47
|
+
|
48
|
+
@timeline.clear
|
49
|
+
|
50
|
+
# This will prevent a transition from happening in the process state_machine
|
51
|
+
throw :halt
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Bluepill
|
3
|
+
module Util
|
4
|
+
class RotationalArray < Array
|
5
|
+
def initialize(size)
|
6
|
+
@capacity = size
|
7
|
+
|
8
|
+
super() # no size - intentionally
|
9
|
+
end
|
10
|
+
|
11
|
+
def push(value)
|
12
|
+
super(value)
|
13
|
+
|
14
|
+
self.shift if self.length > @capacity
|
15
|
+
self
|
16
|
+
end
|
17
|
+
alias_method :<<, :push
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/bluepill.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
require 'bundler/setup' if ENV['BUNDLE_GEMFILE'] && File.exists?(ENV['BUNDLE_GEMFILE'])
|
5
|
+
|
6
|
+
require 'thread'
|
7
|
+
require 'monitor'
|
8
|
+
require 'syslog'
|
9
|
+
require 'timeout'
|
10
|
+
require 'logger'
|
11
|
+
|
12
|
+
require 'active_support/inflector'
|
13
|
+
require 'active_support/core_ext/hash'
|
14
|
+
require 'active_support/core_ext/numeric'
|
15
|
+
require 'active_support/duration'
|
16
|
+
|
17
|
+
require 'bluepill/dsl/process_proxy'
|
18
|
+
require 'bluepill/dsl/process_factory'
|
19
|
+
require 'bluepill/dsl/app_proxy'
|
20
|
+
|
21
|
+
require 'bluepill/application'
|
22
|
+
require 'bluepill/controller'
|
23
|
+
require 'bluepill/socket'
|
24
|
+
require "bluepill/process"
|
25
|
+
require "bluepill/process_statistics"
|
26
|
+
require "bluepill/group"
|
27
|
+
require "bluepill/logger"
|
28
|
+
require "bluepill/condition_watch"
|
29
|
+
require 'bluepill/trigger'
|
30
|
+
require 'bluepill/triggers/flapping'
|
31
|
+
require "bluepill/dsl"
|
32
|
+
require "bluepill/system"
|
33
|
+
|
34
|
+
require "bluepill/process_conditions"
|
35
|
+
|
36
|
+
require "bluepill/util/rotational_array"
|
37
|
+
|
38
|
+
require "bluepill/version"
|
data/local-bluepill
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('lib', File.dirname(__FILE__)))
|
6
|
+
|
7
|
+
require 'optparse'
|
8
|
+
require 'bluepill'
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'rbconfig'
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
RbConfig = Config unless Object.const_defined?(:RbConfig)
|
16
|
+
|
17
|
+
# Default options
|
18
|
+
options = {
|
19
|
+
:log_file => "/var/log/bluepill.log",
|
20
|
+
:base_dir => "/var/run/bluepill",
|
21
|
+
:privileged => true,
|
22
|
+
:timeout => 10,
|
23
|
+
:attempts => 1
|
24
|
+
}
|
25
|
+
|
26
|
+
OptionParser.new do |opts|
|
27
|
+
opts.banner = "Usage: bluepill [app] cmd [options]"
|
28
|
+
opts.on('-l', "--logfile LOGFILE", "Path to logfile, defaults to #{options[:log_file]}") do |file|
|
29
|
+
options[:log_file] = file
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-c', "--base-dir DIR", "Directory to store bluepill socket and pid files, defaults to #{options[:base_dir]}") do |base_dir|
|
33
|
+
options[:base_dir] = base_dir
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-v", "--version") do
|
37
|
+
puts "bluepill, version #{Bluepill::VERSION}"
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("--[no-]privileged", "Allow/disallow to run #{$0} as non-privileged process. disallowed by default") do |v|
|
42
|
+
options[:privileged] = v
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on('-t', '--timeout Seconds', Integer, "Timeout for commands sent to the daemon, in seconds. Defaults to 10.") do |timeout|
|
46
|
+
options[:timeout] = timeout
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('--attempts Count', Integer, "Attempts for commands sent to the daemon, in seconds. Defaults to 1.") do |attempts|
|
50
|
+
options[:attempts] = attempts
|
51
|
+
end
|
52
|
+
|
53
|
+
help = proc do
|
54
|
+
puts opts
|
55
|
+
puts
|
56
|
+
puts "Commands:"
|
57
|
+
puts " load CONFIG_FILE\t\tLoads new instance of bluepill using the specified config file"
|
58
|
+
puts " status\t\t\tLists the status of the proceses for the specified app"
|
59
|
+
puts " start [TARGET]\t\tIssues the start command for the target process or group, defaults to all processes"
|
60
|
+
puts " stop [TARGET]\t\tIssues the stop command for the target process or group, defaults to all processes"
|
61
|
+
puts " restart [TARGET]\t\tIssues the restart command for the target process or group, defaults to all processes"
|
62
|
+
puts " unmonitor [TARGET]\t\tStop monitoring target process or group, defaults to all processes"
|
63
|
+
puts " log [TARGET]\t\tShow the log for the specified process or group, defaults to all for app"
|
64
|
+
puts " quit\t\t\tStop bluepill"
|
65
|
+
puts
|
66
|
+
puts "See http://github.com/arya/bluepill for README"
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on_tail('-h','--help', 'Show this message', &help)
|
71
|
+
help.call if ARGV.empty?
|
72
|
+
end.parse!
|
73
|
+
|
74
|
+
# Check for root
|
75
|
+
if options[:privileged] && ::Process.euid != 0
|
76
|
+
$stderr.puts "You must run bluepill as root or use --no-privileged option."
|
77
|
+
exit(3)
|
78
|
+
end
|
79
|
+
|
80
|
+
APPLICATION_COMMANDS = %w(status start stop restart unmonitor quit log)
|
81
|
+
|
82
|
+
controller = Bluepill::Controller.new(options.slice(:base_dir, :log_file))
|
83
|
+
|
84
|
+
if controller.running_applications.include?(File.basename($0)) && File.symlink?($0)
|
85
|
+
# bluepill was called as a symlink with the name of the target application
|
86
|
+
options[:application] = File.basename($0)
|
87
|
+
elsif controller.running_applications.include?(ARGV.first)
|
88
|
+
# the first arg is the application name
|
89
|
+
options[:application] = ARGV.shift
|
90
|
+
elsif APPLICATION_COMMANDS.include?(ARGV.first)
|
91
|
+
if controller.running_applications.length == 1
|
92
|
+
# there is only one, let's just use that
|
93
|
+
options[:application] = controller.running_applications.first
|
94
|
+
elsif controller.running_applications.length > 1
|
95
|
+
# There is more than one, tell them the list and exit
|
96
|
+
$stderr.puts "You must specify an application name to run that command. Here's the list of running applications:"
|
97
|
+
controller.running_applications.each_with_index do |app, index|
|
98
|
+
$stderr.puts " #{index + 1}. #{app}"
|
99
|
+
end
|
100
|
+
$stderr.puts "Usage: bluepill [app] cmd [options]"
|
101
|
+
exit(1)
|
102
|
+
else
|
103
|
+
# There are none running AND they aren't trying to start one
|
104
|
+
$stderr.puts "Error: There are no running bluepill daemons.\nTo start a bluepill daemon, use: bluepill load <config file>"
|
105
|
+
exit(2)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
options[:command] = ARGV.shift
|
110
|
+
|
111
|
+
if options[:command] == "load"
|
112
|
+
file = ARGV.shift
|
113
|
+
if File.exists?(file)
|
114
|
+
# Restart the ruby interpreter for the config file so that anything loaded here
|
115
|
+
# does not stay in memory for the daemon
|
116
|
+
ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
|
117
|
+
load_path = File.expand_path("#{File.dirname(__FILE__)}/../lib")
|
118
|
+
file_path = File.expand_path(file)
|
119
|
+
|
120
|
+
exec(ruby, "-I#{load_path}", '-rbluepill', file_path)
|
121
|
+
|
122
|
+
else
|
123
|
+
$stderr.puts "Can't find file: #{file}"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
target = ARGV.shift
|
127
|
+
controller.handle_command(options[:application], options[:command], target)
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
describe Bluepill::ProcessStatistics do
|
2
|
+
before(:each) do
|
3
|
+
@stats = Bluepill::ProcessStatistics.new
|
4
|
+
end
|
5
|
+
|
6
|
+
it "should record events" do
|
7
|
+
@stats.record_event('some event', 'some reason')
|
8
|
+
@stats.record_event('another event', 'another reason')
|
9
|
+
@stats.events.should have(2).events
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should record #EVENTS_TO_PERSIST events" do
|
13
|
+
(2 * Bluepill::ProcessStatistics::EVENTS_TO_PERSIST).times do
|
14
|
+
@stats.record_event('some event', 'some reason')
|
15
|
+
end
|
16
|
+
@stats.events.should have(Bluepill::ProcessStatistics::EVENTS_TO_PERSIST).events
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return event history" do
|
20
|
+
@stats.record_event('some event', 'some reason')
|
21
|
+
@stats.to_s.should match(/some reason/)
|
22
|
+
@stats.to_s.should match(/event history/)
|
23
|
+
end
|
24
|
+
end
|