bluepill 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +3 -0
- data/README +0 -0
- data/README.rdoc +18 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/bluepill +53 -0
- data/lib/bluepill.rb +22 -0
- data/lib/bluepill/application.rb +213 -0
- data/lib/bluepill/application/client.rb +7 -0
- data/lib/bluepill/application/server.rb +24 -0
- data/lib/bluepill/comm.rb +5 -0
- data/lib/bluepill/condition_watch.rb +52 -0
- data/lib/bluepill/controller.rb +22 -0
- data/lib/bluepill/dsl.rb +46 -0
- data/lib/bluepill/group.rb +53 -0
- data/lib/bluepill/logger.rb +17 -0
- data/lib/bluepill/process.rb +201 -0
- data/lib/bluepill/process_conditions.rb +7 -0
- data/lib/bluepill/process_conditions/always_true.rb +17 -0
- data/lib/bluepill/process_conditions/cpu_usage.rb +17 -0
- data/lib/bluepill/process_conditions/mem_usage.rb +17 -0
- data/lib/bluepill/process_conditions/process_condition.rb +13 -0
- data/lib/bluepill/socket.rb +43 -0
- data/lib/bluepill/util/rotational_array.rb +34 -0
- data/lib/example.rb +73 -0
- data/spec/blue-pill_spec.rb +7 -0
- data/spec/process_spec.rb +33 -0
- data/spec/spec_helper.rb +13 -0
- metadata +129 -0
data/.document
ADDED
data/LICENSE
ADDED
data/README
ADDED
File without changes
|
data/README.rdoc
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= little-blue-pill
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but
|
13
|
+
bump version in a commit by itself I can ignore when I pull)
|
14
|
+
* Send me a pull request. Bonus points for topic branches.
|
15
|
+
|
16
|
+
== Copyright
|
17
|
+
|
18
|
+
Copyright (c) 2009 garru. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "bluepill"
|
8
|
+
gem.summary = %Q{A process monitor written in Ruby with stability and minimalism in mind.}
|
9
|
+
gem.description = %Q{Bluepill keeps your daemons up while taking up as little resources as possible. After all you probably want the resources of your server to be used by whatever daemons you are running rather than the thing that's supposed to make sure they are brought back up, should they die or misbehave.}
|
10
|
+
gem.email = "entombedvirus@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/arya/bluepill"
|
12
|
+
gem.authors = ["Arya Asemanfar", "Gary Tsang", "Rohith Ravi"]
|
13
|
+
gem.add_development_dependency "rspec"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
gem.add_dependency("daemons", ">= 1.0.9")
|
16
|
+
gem.add_dependency("pluginaweek-state_machine", ">= 0.8.0")
|
17
|
+
gem.add_dependency("activesupport", ">= 2.0.2")
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
if File.exist?('VERSION')
|
42
|
+
version = File.read('VERSION')
|
43
|
+
else
|
44
|
+
version = ""
|
45
|
+
end
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "blue-pill #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/bluepill
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
require 'optparse'
|
4
|
+
require 'bluepill'
|
5
|
+
|
6
|
+
# defaults
|
7
|
+
options = {
|
8
|
+
:application => "all",
|
9
|
+
:log_file => "/var/log/bluepill.log"
|
10
|
+
}
|
11
|
+
|
12
|
+
OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: bluepill [app] cmd [options]"
|
14
|
+
|
15
|
+
opts.on("--logfile LOGFILE") do |file|
|
16
|
+
options[:log_file] = file
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on("--base-dir DIR") do |base_dir|
|
20
|
+
options[:base_dir] = base_dir
|
21
|
+
end
|
22
|
+
end.parse!
|
23
|
+
|
24
|
+
ALLOWED_COMMANDS = %w(load status start stop restart log unmonitor quit)
|
25
|
+
|
26
|
+
controller = Bluepill::Controller.new(options.slice(:base_dir))
|
27
|
+
|
28
|
+
if controller.list.include?(ARGV.first)
|
29
|
+
options[:application] = ARGV.shift
|
30
|
+
elsif controller.list.length == 1 && ALLOWED_COMMANDS.include?(ARGV.first)
|
31
|
+
options[:application] = controller.list.first
|
32
|
+
end
|
33
|
+
|
34
|
+
options[:command] = ARGV.shift
|
35
|
+
|
36
|
+
case options[:command]
|
37
|
+
when "load"
|
38
|
+
file = ARGV.shift
|
39
|
+
eval(File.read(file))
|
40
|
+
when "log"
|
41
|
+
pattern = ARGV.shift
|
42
|
+
pattern = controller.send_cmd(options[:application], :grep_pattern, pattern)
|
43
|
+
|
44
|
+
cmd = "tail -n 100 -f #{options[:log_file]} | grep -E '#{pattern}'"
|
45
|
+
Kernel.exec(cmd)
|
46
|
+
|
47
|
+
when *ALLOWED_COMMANDS
|
48
|
+
process_or_group_name = ARGV.shift
|
49
|
+
puts controller.send_cmd(options[:application], options[:command], process_or_group_name)
|
50
|
+
|
51
|
+
else
|
52
|
+
"Unknown command `%s`" % options[:command]
|
53
|
+
end
|
data/lib/bluepill.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'syslog'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
require 'active_support/core_ext/hash'
|
6
|
+
|
7
|
+
require 'bluepill/application'
|
8
|
+
require 'bluepill/controller'
|
9
|
+
require 'bluepill/socket'
|
10
|
+
require "bluepill/process"
|
11
|
+
require "bluepill/group"
|
12
|
+
require "bluepill/logger"
|
13
|
+
require "bluepill/condition_watch"
|
14
|
+
require "bluepill/dsl"
|
15
|
+
|
16
|
+
require "bluepill/process_conditions"
|
17
|
+
require "bluepill/process_conditions/process_condition"
|
18
|
+
require "bluepill/process_conditions/cpu_usage"
|
19
|
+
require "bluepill/process_conditions/mem_usage"
|
20
|
+
require "bluepill/process_conditions/always_true"
|
21
|
+
|
22
|
+
require "bluepill/util/rotational_array"
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Bluepill
|
4
|
+
class Application
|
5
|
+
attr_accessor :name, :logger, :base_dir, :socket, :pid_file
|
6
|
+
attr_accessor :groups, :group_logger
|
7
|
+
|
8
|
+
def initialize(name, options = {})
|
9
|
+
self.name = name
|
10
|
+
self.base_dir = options[:base_dir] ||= '/var/bluepill'
|
11
|
+
|
12
|
+
self.logger = Bluepill::Logger.new
|
13
|
+
self.group_logger = Bluepill::Logger.new(self.logger, "#{self.name}:") if self.logger
|
14
|
+
|
15
|
+
# self.groups = Hash.new { |h,k| h[k] = Group.new(k, :logger => self.group_logger) }
|
16
|
+
self.groups = Hash.new
|
17
|
+
|
18
|
+
self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
|
19
|
+
|
20
|
+
@server = false
|
21
|
+
signal_trap
|
22
|
+
end
|
23
|
+
|
24
|
+
def load
|
25
|
+
start_server
|
26
|
+
end
|
27
|
+
|
28
|
+
def status
|
29
|
+
if(@server)
|
30
|
+
buffer = ""
|
31
|
+
if self.groups.has_key?(nil)
|
32
|
+
self.groups[nil].status.each do |line|
|
33
|
+
buffer << "%s: %s\n" % line
|
34
|
+
end
|
35
|
+
buffer << "\n"
|
36
|
+
end
|
37
|
+
self.groups.keys.compact.sort.each do |name|
|
38
|
+
group = self.groups[name]
|
39
|
+
buffer << "#{name}:\n"
|
40
|
+
group.status.each { |line| buffer << " %s: %s\n" % line }
|
41
|
+
buffer << "\n"
|
42
|
+
end
|
43
|
+
buffer
|
44
|
+
else
|
45
|
+
send_to_server('status')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop(process_or_group_name)
|
50
|
+
if(@server)
|
51
|
+
group = self.groups[process_or_group_name]
|
52
|
+
|
53
|
+
if group
|
54
|
+
group.stop
|
55
|
+
|
56
|
+
else
|
57
|
+
self.groups.values.each do |group|
|
58
|
+
group.stop(process_or_group_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
"ok"
|
62
|
+
else
|
63
|
+
send_to_server("stop:#{process_or_group_name}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def start(process_or_group_name)
|
68
|
+
if(@server)
|
69
|
+
group = self.groups[process_or_group_name]
|
70
|
+
|
71
|
+
if group
|
72
|
+
group.start
|
73
|
+
|
74
|
+
else
|
75
|
+
self.groups.values.each do |group|
|
76
|
+
group.start(process_or_group_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
"ok"
|
80
|
+
else
|
81
|
+
send_to_server("start:#{process_or_group_name}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def restart(process_or_group_name)
|
86
|
+
if(@server)
|
87
|
+
group = self.groups[process_or_group_name]
|
88
|
+
|
89
|
+
if group
|
90
|
+
group.restart
|
91
|
+
|
92
|
+
else
|
93
|
+
self.groups.values.each do |group|
|
94
|
+
group.restart(process_or_group_name)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
"ok"
|
98
|
+
else
|
99
|
+
send_to_server("restart:#{process_or_group_name}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def unmonitor(process_or_group_name)
|
104
|
+
if(@server)
|
105
|
+
group = self.groups[process_or_group_name]
|
106
|
+
|
107
|
+
if group
|
108
|
+
group.unmonitor
|
109
|
+
|
110
|
+
else
|
111
|
+
self.groups.values.each do |group|
|
112
|
+
group.unmonitor(process_or_group_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
"ok"
|
116
|
+
else
|
117
|
+
send_to_server("unmonitor:#{process_or_group_name}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_process(process, group = nil)
|
122
|
+
self.groups[group] ||= Group.new(group, :logger => self.group_logger)
|
123
|
+
self.groups[group].add_process(process)
|
124
|
+
end
|
125
|
+
|
126
|
+
def send_to_server(method)
|
127
|
+
self.socket = Bluepill::Socket.new(name, base_dir).client
|
128
|
+
socket.write(method + "\n")
|
129
|
+
buffer = ""
|
130
|
+
while(line = socket.gets)
|
131
|
+
buffer << line
|
132
|
+
end
|
133
|
+
return buffer
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def listener
|
139
|
+
Thread.new(self) do |app|
|
140
|
+
begin
|
141
|
+
loop do
|
142
|
+
logger.info("Server | Command loop started:")
|
143
|
+
client = socket.accept
|
144
|
+
logger.info("Server: Handling Request")
|
145
|
+
cmd = client.readline.strip
|
146
|
+
logger.info("Server: #{cmd}")
|
147
|
+
response = app.send(*cmd.split(":"))
|
148
|
+
logger.info("Server: Sending Response")
|
149
|
+
client.write(response)
|
150
|
+
client.close
|
151
|
+
end
|
152
|
+
rescue Exception => e
|
153
|
+
logger.info(e.inspect)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def start_server
|
159
|
+
if File.exists?(self.pid_file)
|
160
|
+
previous_pid = File.read(self.pid_file).to_i
|
161
|
+
begin
|
162
|
+
puts "Killing previous bluepilld[#{previous_pid}]"
|
163
|
+
::Process.kill(2, previous_pid)
|
164
|
+
rescue Exception => e
|
165
|
+
exit unless e.is_a?(Errno::ESRCH)
|
166
|
+
# it was probably already dead
|
167
|
+
end
|
168
|
+
sleep 1 # wait for it to die
|
169
|
+
end
|
170
|
+
|
171
|
+
Daemonize.daemonize
|
172
|
+
|
173
|
+
@server = true
|
174
|
+
self.socket = Bluepill::Socket.new(name, base_dir).server
|
175
|
+
File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
|
176
|
+
$0 = "bluepilld: #{self.name}"
|
177
|
+
self.groups.each {|name, group| group.start }
|
178
|
+
listener
|
179
|
+
run
|
180
|
+
end
|
181
|
+
|
182
|
+
def run
|
183
|
+
loop do
|
184
|
+
self.groups.each do |_, group|
|
185
|
+
group.tick
|
186
|
+
end
|
187
|
+
sleep 1
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def cleanup
|
192
|
+
# self.socket.cleanup
|
193
|
+
end
|
194
|
+
|
195
|
+
def signal_trap
|
196
|
+
|
197
|
+
terminator = lambda do
|
198
|
+
puts "Terminating..."
|
199
|
+
cleanup
|
200
|
+
::Kernel.exit
|
201
|
+
end
|
202
|
+
|
203
|
+
Signal.trap("TERM", &terminator)
|
204
|
+
Signal.trap("INT", &terminator)
|
205
|
+
end
|
206
|
+
|
207
|
+
def grep_pattern(query)
|
208
|
+
bluepilld = 'bluepill\[[[:digit:]]+\]:[[:space:]]+'
|
209
|
+
pattern = [self.name, query].join('|')
|
210
|
+
[bluepilld, '\[.*', Regexp.escape(pattern), '.*\]'].join
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bluepill
|
2
|
+
module Application
|
3
|
+
module ServerMethods
|
4
|
+
|
5
|
+
def status
|
6
|
+
buffer = ""
|
7
|
+
self.processes.each do | process |
|
8
|
+
buffer << "#{process.name} #{process.state}\n" +
|
9
|
+
end
|
10
|
+
buffer
|
11
|
+
end
|
12
|
+
|
13
|
+
def restart
|
14
|
+
self.socket = Bluepill::Socket.new(name, base_dir).client
|
15
|
+
socket.send("restart\n", 0)
|
16
|
+
end
|
17
|
+
|
18
|
+
def stop
|
19
|
+
self.socket = Bluepill::Socket.new(name, base_dir).client
|
20
|
+
socket.send("stop\n", 0)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Bluepill
|
2
|
+
class ConditionWatch
|
3
|
+
attr_accessor :logger
|
4
|
+
def initialize(name, options = {})
|
5
|
+
@name = name
|
6
|
+
|
7
|
+
@logger = options.delete(:logger)
|
8
|
+
@fires = options.has_key?(:fires) ? [options.delete(:fires)].flatten : [:restart]
|
9
|
+
@every = options.delete(:every)
|
10
|
+
@times = options.delete(:times) || [1,1]
|
11
|
+
@times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
|
12
|
+
|
13
|
+
self.clear_history!
|
14
|
+
|
15
|
+
@process_condition = ProcessConditions.name_to_class(@name).new(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(pid, tick_number = Time.now.to_i)
|
19
|
+
if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
|
20
|
+
@last_ran_at = tick_number
|
21
|
+
self.record_value(@process_condition.run(pid))
|
22
|
+
return @fires if self.fired?
|
23
|
+
end
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
|
27
|
+
def record_value(value)
|
28
|
+
# TODO: record value in ProcessStatistics
|
29
|
+
self.logger.info(self.to_s) if self.logger
|
30
|
+
@history[@history_index] = [value, @process_condition.check(value)]
|
31
|
+
@history_index = (@history_index + 1) % @history.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear_history!
|
35
|
+
@last_ran_at = nil
|
36
|
+
@history = Array.new(@times[1])
|
37
|
+
@history_index = 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def fired?
|
41
|
+
@history.select {|v| !v[1] }.size >= @times[0]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
# TODO: this will be out of order because of the way history values are assigned
|
46
|
+
# use (@history[(@history_index - 1)..1] + @history[0..(@history_index - 1)]).
|
47
|
+
# collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
|
48
|
+
# but that's gross so... it's gonna be out of order till we figure out a better way to get it in order
|
49
|
+
@history.collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Bluepill
|
2
|
+
class Controller
|
3
|
+
attr_accessor :base_dir, :sockets_dir, :pids_dir
|
4
|
+
attr_accessor :applications
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
self.base_dir = options[:base_dir] || '/var/bluepill'
|
8
|
+
self.sockets_dir = File.join(base_dir, 'socks')
|
9
|
+
self.pids_dir = File.join(base_dir, 'pids')
|
10
|
+
self.applications = Hash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def list
|
14
|
+
Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
|
15
|
+
end
|
16
|
+
|
17
|
+
def send_cmd(application, command, *args)
|
18
|
+
applications[application] ||= Application.new(application, {:base_dir => base_dir})
|
19
|
+
applications[application].send(command.to_sym, *args.compact)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/bluepill/dsl.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
module Bluepill
|
3
|
+
def self.application(app_name, options = {}, &block)
|
4
|
+
app = Application.new(app_name.to_s, options, &block)
|
5
|
+
|
6
|
+
process_proxy = Class.new do
|
7
|
+
attr_reader :attributes, :watches
|
8
|
+
def initialize
|
9
|
+
@attributes = {}
|
10
|
+
@watches = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(name, *args)
|
14
|
+
if args.size == 1 && name.to_s =~ /^(.*)=$/
|
15
|
+
@attributes[$1.to_sym] = args.first
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def checks(name, options = {})
|
22
|
+
@watches[name] = options
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
app_proxy = Class.new do
|
27
|
+
@@app = app
|
28
|
+
@@process_proxy = process_proxy
|
29
|
+
|
30
|
+
def process(process_name, &process_block)
|
31
|
+
process_proxy = @@process_proxy.new
|
32
|
+
process_block.call(process_proxy)
|
33
|
+
group = process_proxy.attributes.delete(:group)
|
34
|
+
process = Bluepill::Process.new(process_name, process_proxy.attributes)
|
35
|
+
process_proxy.watches.each do |name, opts|
|
36
|
+
process.add_watch(name, opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
@@app.add_process(process, group)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
yield(app_proxy.new)
|
44
|
+
app.load
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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
|
+
|
11
|
+
if self.logger
|
12
|
+
logger_prefix = self.name ? "#{self.name}:" : nil
|
13
|
+
self.process_logger = Bluepill::Logger.new(self.logger, logger_prefix)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_process(process)
|
18
|
+
process.logger = Logger.new(self.process_logger, process.name)
|
19
|
+
self.processes << process
|
20
|
+
end
|
21
|
+
|
22
|
+
def tick
|
23
|
+
self.each_process do |process|
|
24
|
+
process.tick
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# proxied events
|
29
|
+
[:start, :unmonitor, :stop, :restart].each do |event|
|
30
|
+
eval <<-END
|
31
|
+
def #{event}(process_name = nil)
|
32
|
+
self.each_process do |process|
|
33
|
+
process.dispatch!("#{event}") if process_name.nil? || process.name == process_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
END
|
37
|
+
end
|
38
|
+
|
39
|
+
def status
|
40
|
+
status = []
|
41
|
+
self.each_process do |process|
|
42
|
+
status << [process.name, process.state]
|
43
|
+
end
|
44
|
+
status
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
protected
|
49
|
+
def each_process(&block)
|
50
|
+
self.processes.each(&block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bluepill
|
2
|
+
class Logger
|
3
|
+
def initialize(logger = nil, prefix = nil)
|
4
|
+
@logger = logger || Syslog.open('bluepill', Syslog::LOG_PID | Syslog::LOG_CONS, Syslog::LOG_LOCAL6)
|
5
|
+
@prefix = prefix
|
6
|
+
end
|
7
|
+
|
8
|
+
[:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug].each do |method|
|
9
|
+
eval <<-END
|
10
|
+
def #{method}(*args)
|
11
|
+
with_prefix = args.collect {|s| "\#{@prefix}\#{s}" }
|
12
|
+
@logger.#{method}(*with_prefix)
|
13
|
+
end
|
14
|
+
END
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require "state_machine"
|
2
|
+
require "daemons"
|
3
|
+
|
4
|
+
module Bluepill
|
5
|
+
class Process
|
6
|
+
CONFIGURABLE_ATTRIBUTES = [:start_command, :stop_command, :restart_command, :daemonize, :pid_file, :start_grace_time, :stop_grace_time, :restart_grace_time]
|
7
|
+
|
8
|
+
attr_accessor :name, :watches, :logger, :skip_ticks_until
|
9
|
+
attr_accessor *CONFIGURABLE_ATTRIBUTES
|
10
|
+
|
11
|
+
state_machine :initial => :unmonitored do
|
12
|
+
state :unmonitored, :up, :down
|
13
|
+
|
14
|
+
event :tick do
|
15
|
+
transition :unmonitored => :unmonitored
|
16
|
+
|
17
|
+
transition :up => :up, :if => :process_running?
|
18
|
+
transition :up => :down, :unless => :process_running?
|
19
|
+
|
20
|
+
transition :down => :up, :if => lambda {|process| process.process_running? || process.start_process }
|
21
|
+
end
|
22
|
+
|
23
|
+
event :start do
|
24
|
+
transition :unmonitored => :up, :if => lambda {|process| process.process_running? || process.start_process }
|
25
|
+
transition :up => :up
|
26
|
+
transition :down => :up, :if => :start_process
|
27
|
+
end
|
28
|
+
|
29
|
+
event :stop do
|
30
|
+
transition [:unmonitored, :down] => :unmonitored
|
31
|
+
transition :up => :unmonitored, :if => :stop_process
|
32
|
+
end
|
33
|
+
|
34
|
+
event :restart do
|
35
|
+
transition all => :up, :if => :restart_process
|
36
|
+
end
|
37
|
+
|
38
|
+
event :unmonitor do
|
39
|
+
transition all => :unmonitored
|
40
|
+
end
|
41
|
+
|
42
|
+
after_transition any => any do |process, transition|
|
43
|
+
unless transition.loopback?
|
44
|
+
process.record_transition(transition.to_name)
|
45
|
+
# process.logger.info "Going from #{transition.from_name} => #{transition.to_name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
def tick
|
52
|
+
return if self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
|
53
|
+
self.skip_ticks_until = nil
|
54
|
+
|
55
|
+
# clear the momoization per tick
|
56
|
+
@process_running = nil
|
57
|
+
|
58
|
+
# run state machine transitions
|
59
|
+
super
|
60
|
+
|
61
|
+
|
62
|
+
if process_running?
|
63
|
+
run_watches
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(process_name, options = {})
|
68
|
+
@name = process_name
|
69
|
+
@transition_history = Util::RotationalArray.new(10)
|
70
|
+
@watches = []
|
71
|
+
|
72
|
+
@stop_grace_time = @start_grace_time = @restart_grace_time = 3
|
73
|
+
|
74
|
+
CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
|
75
|
+
self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
|
76
|
+
end
|
77
|
+
|
78
|
+
raise ArgumentError, "Please specify a pid_file or the demonize option" if pid_file.nil? && !daemonize?
|
79
|
+
|
80
|
+
# Let state_machine do its initialization stuff
|
81
|
+
super()
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_watch(name, options = {})
|
85
|
+
self.watches << ConditionWatch.new(name, options.merge(:logger => self.watch_logger))
|
86
|
+
end
|
87
|
+
|
88
|
+
def daemonize?
|
89
|
+
!!self.daemonize
|
90
|
+
end
|
91
|
+
|
92
|
+
def dispatch!(event)
|
93
|
+
logger.info "Got stop"
|
94
|
+
self.send("#{event}!")
|
95
|
+
end
|
96
|
+
|
97
|
+
def process_running?(force = false)
|
98
|
+
@process_running = nil if force
|
99
|
+
@process_running ||= signal_process(0)
|
100
|
+
end
|
101
|
+
|
102
|
+
def start_process
|
103
|
+
self.clear_pid
|
104
|
+
if daemonize?
|
105
|
+
starter = lambda {::Kernel.exec(start_command)}
|
106
|
+
child_pid = Daemonize.call_as_daemon(starter)
|
107
|
+
File.open(pid_file, "w") {|f| f.write(child_pid)}
|
108
|
+
|
109
|
+
else
|
110
|
+
# This is a self-daemonizing process
|
111
|
+
system(start_command)
|
112
|
+
end
|
113
|
+
|
114
|
+
skip_ticks_for(start_grace_time)
|
115
|
+
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def stop_process
|
120
|
+
self.clear_pid
|
121
|
+
if stop_command
|
122
|
+
system(stop_command)
|
123
|
+
else
|
124
|
+
signal_process("TERM")
|
125
|
+
|
126
|
+
wait_until = Time.now.to_i + stop_grace_time
|
127
|
+
while process_running?(true)
|
128
|
+
if wait_until <= Time.now.to_i
|
129
|
+
signal_process("KILL")
|
130
|
+
break
|
131
|
+
end
|
132
|
+
sleep 0.1
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
skip_ticks_for(stop_grace_time)
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def restart_process
|
142
|
+
self.clear_pid
|
143
|
+
if restart_command
|
144
|
+
system(restart_command)
|
145
|
+
skip_ticks_for(restart_grace_time)
|
146
|
+
|
147
|
+
else
|
148
|
+
stop_process
|
149
|
+
start_process
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def skip_ticks_for(seconds)
|
154
|
+
self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds
|
155
|
+
end
|
156
|
+
|
157
|
+
def run_watches
|
158
|
+
now = Time.now.to_i
|
159
|
+
|
160
|
+
threads = self.watches.collect do |watch|
|
161
|
+
Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }
|
162
|
+
end
|
163
|
+
|
164
|
+
@transitioned = false
|
165
|
+
|
166
|
+
threads.inject([]) do |events, thread|
|
167
|
+
thread.join
|
168
|
+
events << thread[:events]
|
169
|
+
end.flatten.uniq.each do |event|
|
170
|
+
break if @transitioned
|
171
|
+
self.dispatch!(event)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def record_transition(state_name)
|
176
|
+
@transitioned = true
|
177
|
+
# do other stuff here?
|
178
|
+
end
|
179
|
+
|
180
|
+
def signal_process(code)
|
181
|
+
::Process.kill(code, actual_pid)
|
182
|
+
true
|
183
|
+
rescue
|
184
|
+
false
|
185
|
+
end
|
186
|
+
|
187
|
+
def actual_pid
|
188
|
+
@actual_pid ||= File.read(pid_file).to_i if File.exists?(pid_file)
|
189
|
+
end
|
190
|
+
|
191
|
+
def clear_pid
|
192
|
+
@actual_pid = nil
|
193
|
+
File.unlink(pid_file) if File.exists?(pid_file)
|
194
|
+
end
|
195
|
+
|
196
|
+
def watch_logger
|
197
|
+
@watch_logger ||= Logger.new(self.logger, "#{self.name}:") if self.logger
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bluepill
|
2
|
+
module ProcessConditions
|
3
|
+
class CpuUsage < ProcessCondition
|
4
|
+
def initialize(options = {})
|
5
|
+
@below = options[:below]
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(pid)
|
9
|
+
`ps ux -p #{pid} | tail -1 | awk '{print $3}'`.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def check(value)
|
13
|
+
value < @below
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bluepill
|
2
|
+
module ProcessConditions
|
3
|
+
class MemUsage < ProcessCondition
|
4
|
+
def initialize(options = {})
|
5
|
+
@below = options[:below]
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(pid)
|
9
|
+
`ps ux -p #{pid} | tail -1 | awk '{print $5}'`.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def check(value)
|
13
|
+
value < @below
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Bluepill
|
4
|
+
class Socket
|
5
|
+
attr_accessor :name, :base_dir, :socket
|
6
|
+
|
7
|
+
def initialize(name, base_dir)
|
8
|
+
self.name = name
|
9
|
+
self.base_dir = base_dir
|
10
|
+
@isserver = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def client
|
14
|
+
self.socket = UNIXSocket.open(socket_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def server
|
18
|
+
@isserver = true
|
19
|
+
begin
|
20
|
+
self.socket = UNIXServer.open(socket_name)
|
21
|
+
rescue Errno::EADDRINUSE
|
22
|
+
#if sock file has been created. test to see if there is a server
|
23
|
+
tmp_socket = UNIXSocket.open(socket_name) rescue nil
|
24
|
+
if tmp_socket.nil?
|
25
|
+
cleanup
|
26
|
+
retry
|
27
|
+
else
|
28
|
+
raise Exception.new("Server is already running")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cleanup
|
34
|
+
File.delete(socket_name) if @isserver
|
35
|
+
end
|
36
|
+
|
37
|
+
def socket_name
|
38
|
+
@socket_name ||= File.join(base_dir, 'socks', name + ".sock")
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Bluepill
|
2
|
+
module Util
|
3
|
+
class RotationalArray < Array
|
4
|
+
def initialize(size)
|
5
|
+
super
|
6
|
+
@index = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def push(value)
|
10
|
+
self[@index] = value
|
11
|
+
@index = (@index + 1) % self.size
|
12
|
+
puts @index
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :<<, :push
|
16
|
+
|
17
|
+
def pop
|
18
|
+
raise "Cannot call pop on a rotational array"
|
19
|
+
end
|
20
|
+
|
21
|
+
def shift
|
22
|
+
raise "Cannot call shift on a rotational array"
|
23
|
+
end
|
24
|
+
|
25
|
+
def unshift
|
26
|
+
raise "Cannot call unshift on a rotational array"
|
27
|
+
end
|
28
|
+
|
29
|
+
def last
|
30
|
+
self[@index - 1]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/example.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bluepill'
|
3
|
+
|
4
|
+
ROOT_DIR = "/tmp/bp"
|
5
|
+
|
6
|
+
# application = Bluepill::Application.new("poop", 'base_dir' => '/tmp/bp')
|
7
|
+
#
|
8
|
+
# process = Bluepill::Process.new("hello_world") do |process|
|
9
|
+
# process.start_command = "sleep 5"
|
10
|
+
# process.daemonize = true
|
11
|
+
# process.pid_file = "/tmp/bp/sleep.pid"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# process.add_watch("AlwaysTrue", :every => 5)
|
15
|
+
#
|
16
|
+
# application.processes << process
|
17
|
+
# process.dispatch!("start")
|
18
|
+
#
|
19
|
+
# application.start
|
20
|
+
|
21
|
+
|
22
|
+
Bluepill.application(:sample_app, :base_dir => ROOT_DIR) do |app|
|
23
|
+
2.times do |i|
|
24
|
+
app.process("process_#{i}") do |process|
|
25
|
+
process.start_command = "echo 'Process #{i}' && sleep #{rand(15) + i}"
|
26
|
+
process.daemonize = true
|
27
|
+
process.pid_file = "#{ROOT_DIR}/pids/process_#{i}.pid"
|
28
|
+
|
29
|
+
process.checks :always_true, :every => 2
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
10.times do |i|
|
34
|
+
app.process("group_process_#{i}") do |process|
|
35
|
+
process.start_command = "sleep #{rand(15) + i}"
|
36
|
+
process.group = "Poopfaced"
|
37
|
+
process.daemonize = true
|
38
|
+
process.pid_file = "#{ROOT_DIR}/pids/process_#{i}.pid"
|
39
|
+
|
40
|
+
process.checks :always_true, :every => 2
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Bluepill.watch do
|
47
|
+
# start_command "start_process -P file.pid"
|
48
|
+
# stop_command "stop_process -P file.pid"
|
49
|
+
# pid_file 'file.pid'
|
50
|
+
#
|
51
|
+
# checks do |checks|
|
52
|
+
# checks.mem_usage :every => 15.minutes,
|
53
|
+
# :below => 250.megabytes,
|
54
|
+
# :fires => :restart
|
55
|
+
#
|
56
|
+
# checks.cpu_usage :every 10.seconds,
|
57
|
+
# :below => 50.percent,
|
58
|
+
# :fires => :restart
|
59
|
+
#
|
60
|
+
# checks.custom_method :custom_params => :to_be_sent_to_the_custom_condition,
|
61
|
+
# :fires => [:stop, :custom_event, :start]
|
62
|
+
#
|
63
|
+
# checks.deadly_condition :every => 20.seconds,
|
64
|
+
# :fires => :stop
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# handles(:restart) do |process|
|
68
|
+
# # process has pid
|
69
|
+
# process.transition :down
|
70
|
+
# process.transition :up
|
71
|
+
# run "some commands -P #{process.pid}"
|
72
|
+
# end
|
73
|
+
# end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Bluepill:Process" do
|
4
|
+
it "should raise exceptions unless properly initialized" do
|
5
|
+
lambda {
|
6
|
+
Bluepill::Process.new
|
7
|
+
}.should raise_error(ArgumentError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should construct a valid object when properly initialized" do
|
11
|
+
lambda {
|
12
|
+
Bluepill::Process.new("test_process") do |p|
|
13
|
+
# The absolute minimum to construct a valid process
|
14
|
+
p.start_command = "/dev/null"
|
15
|
+
p.pid_file = "/var/run/test_process.pid"
|
16
|
+
end
|
17
|
+
}.should_not raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "A Bluepill::Process object" do
|
23
|
+
before(:each) do
|
24
|
+
@process = Bluepill::Process.new("test_process") do |p|
|
25
|
+
p.start_command = "hai"
|
26
|
+
p.daemonize = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be in the unmonitored state after construction" do
|
31
|
+
@process.should be_unmonitored
|
32
|
+
end
|
33
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
5
|
+
|
6
|
+
require 'bluepill'
|
7
|
+
require 'spec'
|
8
|
+
require 'spec/autorun'
|
9
|
+
require "ruby-debug"
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bluepill
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arya Asemanfar
|
8
|
+
- Gary Tsang
|
9
|
+
- Rohith Ravi
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2009-10-05 00:00:00 -07:00
|
15
|
+
default_executable: bluepill
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: rspec
|
19
|
+
type: :development
|
20
|
+
version_requirement:
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: "0"
|
26
|
+
version:
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: daemons
|
29
|
+
type: :runtime
|
30
|
+
version_requirement:
|
31
|
+
version_requirements: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 1.0.9
|
36
|
+
version:
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: pluginaweek-state_machine
|
39
|
+
type: :runtime
|
40
|
+
version_requirement:
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.8.0
|
46
|
+
version:
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: activesupport
|
49
|
+
type: :runtime
|
50
|
+
version_requirement:
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 2.0.2
|
56
|
+
version:
|
57
|
+
description: Bluepill keeps your daemons up while taking up as little resources as possible. After all you probably want the resources of your server to be used by whatever daemons you are running rather than the thing that's supposed to make sure they are brought back up, should they die or misbehave.
|
58
|
+
email: entombedvirus@gmail.com
|
59
|
+
executables:
|
60
|
+
- bluepill
|
61
|
+
extensions: []
|
62
|
+
|
63
|
+
extra_rdoc_files:
|
64
|
+
- LICENSE
|
65
|
+
- README
|
66
|
+
- README.rdoc
|
67
|
+
files:
|
68
|
+
- .document
|
69
|
+
- .gitignore
|
70
|
+
- LICENSE
|
71
|
+
- README
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- VERSION
|
75
|
+
- bin/bluepill
|
76
|
+
- lib/bluepill.rb
|
77
|
+
- lib/bluepill/application.rb
|
78
|
+
- lib/bluepill/application/client.rb
|
79
|
+
- lib/bluepill/application/server.rb
|
80
|
+
- lib/bluepill/comm.rb
|
81
|
+
- lib/bluepill/condition_watch.rb
|
82
|
+
- lib/bluepill/controller.rb
|
83
|
+
- lib/bluepill/dsl.rb
|
84
|
+
- lib/bluepill/group.rb
|
85
|
+
- lib/bluepill/logger.rb
|
86
|
+
- lib/bluepill/process.rb
|
87
|
+
- lib/bluepill/process_conditions.rb
|
88
|
+
- lib/bluepill/process_conditions/always_true.rb
|
89
|
+
- lib/bluepill/process_conditions/cpu_usage.rb
|
90
|
+
- lib/bluepill/process_conditions/mem_usage.rb
|
91
|
+
- lib/bluepill/process_conditions/process_condition.rb
|
92
|
+
- lib/bluepill/socket.rb
|
93
|
+
- lib/bluepill/util/rotational_array.rb
|
94
|
+
- lib/example.rb
|
95
|
+
- spec/blue-pill_spec.rb
|
96
|
+
- spec/process_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
has_rdoc: true
|
99
|
+
homepage: http://github.com/arya/bluepill
|
100
|
+
licenses: []
|
101
|
+
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options:
|
104
|
+
- --charset=UTF-8
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: "0"
|
112
|
+
version:
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: "0"
|
118
|
+
version:
|
119
|
+
requirements: []
|
120
|
+
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 1.3.2
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: A process monitor written in Ruby with stability and minimalism in mind.
|
126
|
+
test_files:
|
127
|
+
- spec/blue-pill_spec.rb
|
128
|
+
- spec/process_spec.rb
|
129
|
+
- spec/spec_helper.rb
|