bluepill-rwgps 0.0.60

Sign up to get free protection for your applications and to get access to all the features.
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