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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/DESIGN.md +10 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE +22 -0
  7. data/README.md +10 -0
  8. data/Rakefile +38 -0
  9. data/bin/bluepill +124 -0
  10. data/bin/bpsv +3 -0
  11. data/bin/sample_forking_server +53 -0
  12. data/bluepill-rwgps.gemspec +36 -0
  13. data/examples/example.rb +87 -0
  14. data/examples/new_example.rb +89 -0
  15. data/examples/new_runit_example.rb +29 -0
  16. data/examples/runit_example.rb +26 -0
  17. data/lib/bluepill/application/client.rb +8 -0
  18. data/lib/bluepill/application/server.rb +23 -0
  19. data/lib/bluepill/application.rb +205 -0
  20. data/lib/bluepill/condition_watch.rb +50 -0
  21. data/lib/bluepill/controller.rb +121 -0
  22. data/lib/bluepill/dsl/app_proxy.rb +25 -0
  23. data/lib/bluepill/dsl/process_factory.rb +122 -0
  24. data/lib/bluepill/dsl/process_proxy.rb +44 -0
  25. data/lib/bluepill/dsl.rb +12 -0
  26. data/lib/bluepill/group.rb +72 -0
  27. data/lib/bluepill/logger.rb +63 -0
  28. data/lib/bluepill/process.rb +490 -0
  29. data/lib/bluepill/process_conditions/always_true.rb +18 -0
  30. data/lib/bluepill/process_conditions/cpu_usage.rb +19 -0
  31. data/lib/bluepill/process_conditions/http.rb +58 -0
  32. data/lib/bluepill/process_conditions/mem_usage.rb +32 -0
  33. data/lib/bluepill/process_conditions/process_condition.rb +22 -0
  34. data/lib/bluepill/process_conditions.rb +14 -0
  35. data/lib/bluepill/process_statistics.rb +27 -0
  36. data/lib/bluepill/socket.rb +58 -0
  37. data/lib/bluepill/system.rb +238 -0
  38. data/lib/bluepill/trigger.rb +60 -0
  39. data/lib/bluepill/triggers/flapping.rb +56 -0
  40. data/lib/bluepill/util/rotational_array.rb +20 -0
  41. data/lib/bluepill/version.rb +4 -0
  42. data/lib/bluepill.rb +38 -0
  43. data/local-bluepill +129 -0
  44. data/spec/lib/bluepill/logger_spec.rb +3 -0
  45. data/spec/lib/bluepill/process_statistics_spec.rb +24 -0
  46. data/spec/lib/bluepill/system_spec.rb +36 -0
  47. data/spec/spec_helper.rb +19 -0
  48. metadata +264 -0
@@ -0,0 +1,63 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ class Logger
4
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ @logger = options[:logger] || self.create_logger
9
+ @prefix = options[:prefix]
10
+ @stdout = options[:stdout]
11
+ @prefixes = {}
12
+ end
13
+
14
+ LOG_METHODS.each do |method|
15
+ eval <<-END
16
+ def #{method}(msg, prefix = [])
17
+ if @logger.is_a?(self.class)
18
+ @logger.#{method}(msg, [@prefix] + prefix)
19
+ else
20
+ s_prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
21
+ if @stdout
22
+ $stdout.puts("[#{method}]: \#{s_prefix}\#{msg}")
23
+ $stdout.flush
24
+ end
25
+ @logger.#{method}("\#{s_prefix}\#{msg}")
26
+ end
27
+ end
28
+ END
29
+ end
30
+
31
+ def prefix_with(prefix)
32
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
33
+ end
34
+
35
+ def reopen
36
+ if @logger.is_a?(self.class)
37
+ @logger.reopen
38
+ else
39
+ @logger = create_logger
40
+ end
41
+ end
42
+
43
+ protected
44
+ def create_logger
45
+ if @options[:log_file]
46
+ LoggerAdapter.new(@options[:log_file])
47
+ else
48
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
49
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
50
+ end
51
+ end
52
+
53
+ class LoggerAdapter < ::Logger
54
+ LOGGER_EQUIVALENTS =
55
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
56
+
57
+ LOG_METHODS.each do |method|
58
+ next if method == LOGGER_EQUIVALENTS[method]
59
+ alias_method method, LOGGER_EQUIVALENTS[method]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,490 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # fixes problem with loading on systems with rubyist-aasm installed
4
+ gem "state_machine"
5
+
6
+ require "state_machine"
7
+ require "daemons"
8
+
9
+ module Bluepill
10
+ class Process
11
+ CONFIGURABLE_ATTRIBUTES = [
12
+ :pre_start_command,
13
+ :start_command,
14
+ :stop_command,
15
+ :restart_command,
16
+
17
+ :stdout,
18
+ :stderr,
19
+ :stdin,
20
+
21
+ :daemonize,
22
+ :pid_file,
23
+ :working_dir,
24
+ :environment,
25
+
26
+ :start_grace_time,
27
+ :stop_grace_time,
28
+ :restart_grace_time,
29
+
30
+ :uid,
31
+ :gid,
32
+
33
+ :cache_actual_pid,
34
+
35
+ :monitor_children,
36
+ :child_process_factory,
37
+
38
+ :pid_command,
39
+ :auto_start,
40
+
41
+ :supplementary_groups,
42
+
43
+ :stop_signals
44
+ ]
45
+
46
+ attr_accessor :name, :watches, :triggers, :logger, :skip_ticks_until, :process_running
47
+ attr_accessor *CONFIGURABLE_ATTRIBUTES
48
+ attr_reader :children, :statistics
49
+
50
+ state_machine :initial => :unmonitored do
51
+ # These are the idle states, i.e. only an event (either external or internal) will trigger a transition.
52
+ # The distinction between down and unmonitored is that down
53
+ # means we know it is not running and unmonitored is that we don't care if it's running.
54
+ state :unmonitored, :up, :down
55
+
56
+ # These are transitionary states, we expect the process to change state after a certain period of time.
57
+ state :starting, :stopping, :restarting
58
+
59
+ event :tick do
60
+ transition :starting => :up, :if => :process_running?
61
+ transition :starting => :down, :unless => :process_running?
62
+
63
+ transition :up => :up, :if => :process_running?
64
+ transition :up => :down, :unless => :process_running?
65
+
66
+ # The process failed to die after entering the stopping state. Change the state to reflect
67
+ # reality.
68
+ transition :stopping => :up, :if => :process_running?
69
+ transition :stopping => :down, :unless => :process_running?
70
+
71
+ transition :down => :up, :if => :process_running?
72
+ transition :down => :starting, :unless => :process_running?
73
+
74
+ transition :restarting => :up, :if => :process_running?
75
+ transition :restarting => :down, :unless => :process_running?
76
+ end
77
+
78
+ event :start do
79
+ transition [:unmonitored, :down] => :starting
80
+ end
81
+
82
+ event :stop do
83
+ transition :up => :stopping
84
+ end
85
+
86
+ event :unmonitor do
87
+ transition any => :unmonitored
88
+ end
89
+
90
+ event :restart do
91
+ transition [:up, :down] => :restarting
92
+ end
93
+
94
+ before_transition any => any, :do => :notify_triggers
95
+ before_transition :stopping => any, :do => :clean_threads
96
+
97
+ after_transition any => :starting, :do => :start_process
98
+ after_transition any => :stopping, :do => :stop_process
99
+ after_transition any => :restarting, :do => :restart_process
100
+
101
+ after_transition any => any, :do => :record_transition
102
+ end
103
+
104
+ def initialize(process_name, checks, options = {})
105
+ @name = process_name
106
+ @event_mutex = Monitor.new
107
+ @watches = []
108
+ @triggers = []
109
+ @children = []
110
+ @threads = []
111
+ @statistics = ProcessStatistics.new
112
+ @actual_pid = options[:actual_pid]
113
+ self.logger = options[:logger]
114
+
115
+ checks.each do |name, opts|
116
+ if Trigger[name]
117
+ self.add_trigger(name, opts)
118
+ else
119
+ self.add_watch(name, opts)
120
+ end
121
+ end
122
+
123
+ # These defaults are overriden below if it's configured to be something else.
124
+ @monitor_children = false
125
+ @cache_actual_pid = true
126
+ @start_grace_time = @stop_grace_time = @restart_grace_time = 3
127
+ @environment = {}
128
+
129
+ CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
130
+ self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
131
+ end
132
+
133
+ # Let state_machine do its initialization stuff
134
+ super() # no arguments intentional
135
+ end
136
+
137
+ def tick
138
+ return if self.skipping_ticks?
139
+ self.skip_ticks_until = nil
140
+
141
+ # clear the memoization per tick
142
+ @process_running = nil
143
+
144
+ # Deal with thread cleanup here since the stopping state isn't used
145
+ clean_threads if self.unmonitored?
146
+
147
+ # run state machine transitions
148
+ super
149
+
150
+ if self.up?
151
+ self.run_watches
152
+
153
+ if self.monitor_children?
154
+ refresh_children!
155
+ children.each {|child| child.tick}
156
+ end
157
+ end
158
+ end
159
+
160
+ def logger=(logger)
161
+ @logger = logger
162
+ self.watches.each {|w| w.logger = logger }
163
+ self.triggers.each {|t| t.logger = logger }
164
+ end
165
+
166
+ # State machine methods
167
+ def dispatch!(event, reason = nil)
168
+ @event_mutex.synchronize do
169
+ @statistics.record_event(event, reason)
170
+ self.send("#{event}")
171
+ end
172
+ end
173
+
174
+ def record_transition(transition)
175
+ unless transition.loopback?
176
+ @transitioned = true
177
+
178
+ # When a process changes state, we should clear the memory of all the watches
179
+ self.watches.each { |w| w.clear_history! }
180
+
181
+ # Also, when a process changes state, we should re-populate its child list
182
+ if self.monitor_children?
183
+ self.logger.warning "Clearing child list"
184
+ self.children.clear
185
+ end
186
+ logger.info "Going from #{transition.from_name} => #{transition.to_name}"
187
+ end
188
+ end
189
+
190
+ def notify_triggers(transition)
191
+ self.triggers.each {|trigger| trigger.notify(transition)}
192
+ end
193
+
194
+ # Watch related methods
195
+ def add_watch(name, options = {})
196
+ self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
197
+ end
198
+
199
+ def add_trigger(name, options = {})
200
+ self.triggers << Trigger[name].new(self, options.merge(:logger => self.logger))
201
+ end
202
+
203
+ def run_watches
204
+ now = Time.now.to_i
205
+
206
+ threads = self.watches.collect do |watch|
207
+ [watch, Thread.new { Thread.current[:events] = watch.run(self.actual_pid, now) }]
208
+ end
209
+
210
+ @transitioned = false
211
+
212
+ threads.inject([]) do |events, (watch, thread)|
213
+ thread.join
214
+ if thread[:events].size > 0
215
+ logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
216
+ thread[:events].each do |event|
217
+ events << [event, watch.to_s]
218
+ end
219
+ end
220
+ events
221
+ end.each do |(event, reason)|
222
+ break if @transitioned
223
+ self.dispatch!(event, reason)
224
+ end
225
+ end
226
+
227
+ def determine_initial_state
228
+ if self.process_running?(true)
229
+ self.state = 'up'
230
+ else
231
+ self.state = (auto_start == false) ? 'unmonitored' : 'down' # we need to check for false value
232
+ end
233
+ end
234
+
235
+ def handle_user_command(cmd)
236
+ case cmd
237
+ when "start"
238
+ if self.process_running?(true)
239
+ logger.warning("Refusing to re-run start command on an already running process.")
240
+ else
241
+ dispatch!(:start, "user initiated")
242
+ end
243
+ when "stop"
244
+ stop_process
245
+ dispatch!(:unmonitor, "user initiated")
246
+ when "restart"
247
+ restart_process
248
+ when "unmonitor"
249
+ # When the user issues an unmonitor cmd, reset any triggers so that
250
+ # scheduled events gets cleared
251
+ triggers.each {|t| t.reset! }
252
+ dispatch!(:unmonitor, "user initiated")
253
+ end
254
+ end
255
+
256
+ # System Process Methods
257
+ def process_running?(force = false)
258
+ @process_running = nil if force # clear existing state if forced
259
+
260
+ @process_running ||= signal_process(0)
261
+ # the process isn't running, so we should clear the PID
262
+ self.clear_pid unless @process_running
263
+ @process_running
264
+ end
265
+
266
+ def start_process
267
+ pre_start_process
268
+ logger.warning "Executing start command: #{start_command}"
269
+ if self.daemonize?
270
+ System.daemonize(start_command, self.system_command_options)
271
+ else
272
+ # This is a self-daemonizing process
273
+ with_timeout(start_grace_time) do
274
+ result = System.execute_blocking(start_command, self.system_command_options)
275
+
276
+ unless result[:exit_code].zero?
277
+ logger.warning "Start command execution returned non-zero exit code:"
278
+ logger.warning result.inspect
279
+ end
280
+ end
281
+ end
282
+
283
+ self.skip_ticks_for(start_grace_time)
284
+ end
285
+
286
+ def pre_start_process
287
+ return unless pre_start_command
288
+ logger.warning "Executing pre start command: #{pre_start_command}"
289
+ result = System.execute_blocking(pre_start_command, self.system_command_options)
290
+ unless result[:exit_code].zero?
291
+ logger.warning "Pre start command execution returned non-zero exit code:"
292
+ logger.warning result.inspect
293
+ end
294
+ end
295
+
296
+ def stop_process
297
+ if stop_command
298
+ cmd = self.prepare_command(stop_command)
299
+ logger.warning "Executing stop command: #{cmd}"
300
+
301
+ with_timeout(stop_grace_time) do
302
+ result = System.execute_blocking(cmd, self.system_command_options)
303
+
304
+ unless result[:exit_code].zero?
305
+ logger.warning "Stop command execution returned non-zero exit code:"
306
+ logger.warning result.inspect
307
+ end
308
+ end
309
+
310
+ elsif stop_signals
311
+ # issue stop signals with configurable delay between each
312
+ logger.warning "Sending stop signals to #{actual_pid}"
313
+ @threads << Thread.new(self, stop_signals.clone) do |process, stop_signals|
314
+ signal = stop_signals.shift
315
+ logger.info "Sending signal #{signal} to #{process.actual_pid}"
316
+ process.signal_process(signal) # send first signal
317
+
318
+ until stop_signals.empty?
319
+ # we already checked to make sure stop_signals had an odd number of items
320
+ delay = stop_signals.shift
321
+ signal = stop_signals.shift
322
+
323
+ logger.debug "Sleeping for #{delay} seconds"
324
+ sleep delay
325
+ #break unless signal_process(0) #break unless the process can be reached
326
+ unless process.signal_process(0)
327
+ logger.debug "Process has terminated."
328
+ break
329
+ end
330
+ logger.info "Sending signal #{signal} to #{process.actual_pid}"
331
+ process.signal_process(signal)
332
+ end
333
+ end
334
+ else
335
+ logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
336
+ signal_process("TERM")
337
+ end
338
+ self.unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?
339
+
340
+ self.skip_ticks_for(stop_grace_time)
341
+ end
342
+
343
+ def restart_process
344
+ if restart_command
345
+ cmd = self.prepare_command(restart_command)
346
+
347
+ logger.warning "Executing restart command: #{cmd}"
348
+
349
+ with_timeout(restart_grace_time) do
350
+ result = System.execute_blocking(cmd, self.system_command_options)
351
+
352
+ unless result[:exit_code].zero?
353
+ logger.warning "Restart command execution returned non-zero exit code:"
354
+ logger.warning result.inspect
355
+ end
356
+ end
357
+
358
+ self.skip_ticks_for(restart_grace_time)
359
+ else
360
+ logger.warning "No restart_command specified. Must stop and start to restart"
361
+ self.stop_process
362
+ # the tick will bring it back.
363
+ end
364
+ end
365
+
366
+ def clean_threads
367
+ @threads.each { |t| t.kill }
368
+ @threads.clear
369
+ end
370
+
371
+ def daemonize?
372
+ !!self.daemonize
373
+ end
374
+
375
+ def monitor_children?
376
+ !!self.monitor_children
377
+ end
378
+
379
+ def signal_process(code)
380
+ code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol)
381
+ ::Process.kill(code, actual_pid)
382
+ true
383
+ rescue Exception => e
384
+ logger.err "Failed to signal process #{actual_pid} with code #{code}: #{e}"
385
+ false
386
+ end
387
+
388
+ def cache_actual_pid?
389
+ !!@cache_actual_pid
390
+ end
391
+
392
+ def actual_pid
393
+ pid_command ? pid_from_command : pid_from_file
394
+ end
395
+
396
+ def pid_from_file
397
+ return @actual_pid if cache_actual_pid? && @actual_pid
398
+ @actual_pid = begin
399
+ if pid_file
400
+ if File.exists?(pid_file)
401
+ str = File.read(pid_file)
402
+ str.to_i if str.size > 0
403
+ else
404
+ logger.warning("pid_file #{pid_file} does not exist or cannot be read")
405
+ nil
406
+ end
407
+ end
408
+ end
409
+ end
410
+
411
+ def pid_from_command
412
+ pid = %x{#{pid_command}}.strip
413
+ (pid =~ /\A\d+\z/) ? pid.to_i : nil
414
+ end
415
+
416
+ def actual_pid=(pid)
417
+ @actual_pid = pid
418
+ end
419
+
420
+ def clear_pid
421
+ @actual_pid = nil
422
+ end
423
+
424
+ def unlink_pid
425
+ File.unlink(pid_file) if pid_file && File.exists?(pid_file)
426
+ rescue Errno::ENOENT
427
+ end
428
+
429
+ # Internal State Methods
430
+ def skip_ticks_for(seconds)
431
+ # TODO: should this be addative or longest wins?
432
+ # i.e. if two calls for skip_ticks_for come in for 5 and 10, should it skip for 10 or 15?
433
+ self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds.to_i
434
+ end
435
+
436
+ def skipping_ticks?
437
+ self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
438
+ end
439
+
440
+ def refresh_children!
441
+ # First prune the list of dead children
442
+ @children.delete_if {|child| !child.process_running?(true) }
443
+
444
+ # Add new found children to the list
445
+ new_children_pids = System.get_children(self.actual_pid) - @children.map {|child| child.actual_pid}
446
+
447
+ unless new_children_pids.empty?
448
+ logger.info "Existing children: #{@children.collect{|c| c.actual_pid}.join(",")}. Got new children: #{new_children_pids.inspect} for #{actual_pid}"
449
+ end
450
+
451
+ # Construct a new process wrapper for each new found children
452
+ new_children_pids.each do |child_pid|
453
+ name = "<child(pid:#{child_pid})>"
454
+ logger = self.logger.prefix_with(name)
455
+
456
+ child = self.child_process_factory.create_child_process(name, child_pid, logger)
457
+ @children << child
458
+ end
459
+ end
460
+
461
+ def prepare_command(command)
462
+ command.to_s.gsub("{{PID}}", actual_pid.to_s)
463
+ end
464
+
465
+ def system_command_options
466
+ {
467
+ :uid => self.uid,
468
+ :gid => self.gid,
469
+ :working_dir => self.working_dir,
470
+ :environment => self.environment,
471
+ :pid_file => self.pid_file,
472
+ :logger => self.logger,
473
+ :stdin => self.stdin,
474
+ :stdout => self.stdout,
475
+ :stderr => self.stderr,
476
+ :supplementary_groups => self.supplementary_groups
477
+ }
478
+ end
479
+
480
+ def with_timeout(secs, &blk)
481
+ Timeout.timeout(secs.to_f, &blk)
482
+
483
+ rescue Timeout::Error
484
+ logger.err "Execution is taking longer than expected. Unmonitoring."
485
+ logger.err "Did you forget to tell bluepill to daemonize this process?"
486
+ self.dispatch!("unmonitor")
487
+ end
488
+ end
489
+ end
490
+
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class AlwaysTrue < ProcessCondition
5
+ def initialize(options = {})
6
+ @below = options[:below]
7
+ end
8
+
9
+ def run(pid)
10
+ 1
11
+ end
12
+
13
+ def check(value)
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class CpuUsage < ProcessCondition
5
+ def initialize(options = {})
6
+ @below = options[:below]
7
+ end
8
+
9
+ def run(pid)
10
+ # third col in the ps axu output
11
+ System.cpu_usage(pid).to_f
12
+ end
13
+
14
+ def check(value)
15
+ value < @below
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Bluepill
6
+ module ProcessConditions
7
+ class Http < ProcessCondition
8
+ def initialize(options = {})
9
+ @uri = URI.parse(options[:url])
10
+ @kind = case options[:kind]
11
+ when Fixnum then Net::HTTPResponse::CODE_TO_OBJ[options[:kind].to_s]
12
+ when String, Symbol then Net.const_get("HTTP#{options[:kind].to_s.camelize}")
13
+ else
14
+ Net::HTTPSuccess
15
+ end
16
+ @pattern = options[:pattern] || nil
17
+ @open_timeout = (options[:open_timeout] || options[:timeout] || 5).to_i
18
+ @read_timeout = (options[:read_timeout] || options[:timeout] || 5).to_i
19
+ end
20
+
21
+ def run(pid)
22
+ session = Net::HTTP.new(@uri.host, @uri.port)
23
+ if @uri.scheme == 'https'
24
+ require 'net/https'
25
+ session.use_ssl=true
26
+ session.verify_mode = OpenSSL::SSL::VERIFY_NONE
27
+ end
28
+ session.open_timeout = @open_timeout
29
+ session.read_timeout = @read_timeout
30
+ hide_net_http_bug do
31
+ session.start do |http|
32
+ http.get(@uri.path)
33
+ end
34
+ end
35
+ rescue
36
+ $!
37
+ end
38
+
39
+ def check(value)
40
+ return false unless value.kind_of?(@kind)
41
+ return true unless @pattern
42
+ return false unless value.class.body_permitted?
43
+ @pattern === value.body
44
+ end
45
+
46
+ private
47
+ def hide_net_http_bug
48
+ yield
49
+ rescue NoMethodError => e
50
+ if e.to_s =~ /#{Regexp.escape(%q|undefined method `closed?' for nil:NilClass|)}/
51
+ raise Errno::ECONNREFUSED, "Connection refused attempting to contact #{@uri.scheme}://#{@uri.host}:#{@uri.port}"
52
+ else
53
+ raise
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class MemUsage < ProcessCondition
5
+ MB = 1024 ** 2
6
+ FORMAT_STR = "%d%s"
7
+ MB_LABEL = "MB"
8
+ KB_LABEL = "KB"
9
+
10
+ def initialize(options = {})
11
+ @below = options[:below]
12
+ end
13
+
14
+ def run(pid)
15
+ # rss is on the 5th col
16
+ System.memory_usage(pid).to_f
17
+ end
18
+
19
+ def check(value)
20
+ value.kilobytes < @below
21
+ end
22
+
23
+ def format_value(value)
24
+ if value.kilobytes >= MB
25
+ FORMAT_STR % [(value / 1024).round, MB_LABEL]
26
+ else
27
+ FORMAT_STR % [value, KB_LABEL]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ module ProcessConditions
4
+ class ProcessCondition
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def run(pid)
10
+ raise "Implement in subclass!"
11
+ end
12
+
13
+ def check(value)
14
+ raise "Implement in subclass!"
15
+ end
16
+
17
+ def format_value(value)
18
+ value
19
+ end
20
+ end
21
+ end
22
+ end