bluepill 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ Copyright (c) 2009 arya garru
2
+ license tbd
3
+
data/README ADDED
File without changes
@@ -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.
@@ -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
@@ -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
@@ -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,7 @@
1
+ module Bluepill
2
+ module Application
3
+ module Client
4
+
5
+ end
6
+ end
7
+ 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,5 @@
1
+ module Bluepill
2
+ class Comm
3
+
4
+ end
5
+ 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
@@ -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,7 @@
1
+ module Bluepill
2
+ module ProcessConditions
3
+ def self.name_to_class(name)
4
+ "#{self}::#{name.to_s.camelcase}".constantize
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module Bluepill
2
+ module ProcessConditions
3
+ class AlwaysTrue < ProcessCondition
4
+ def initialize(options = {})
5
+ @below = options[:below]
6
+ end
7
+
8
+ def run(pid)
9
+ 1
10
+ end
11
+
12
+ def check(value)
13
+ true
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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,13 @@
1
+ module Bluepill
2
+ module ProcessConditions
3
+ class ProcessCondition
4
+ def run(pid)
5
+ raise "Implement in subclass!"
6
+ end
7
+
8
+ def check(value)
9
+ raise "Implement in subclass!"
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -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,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Bluepill" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ 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
@@ -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