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,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
@@ -0,0 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Bluepill
3
+ VERSION = "0.0.60".freeze
4
+ 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,3 @@
1
+ describe Bluepill::Logger do
2
+
3
+ end
@@ -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