dylanvaughn-bluepill 0.0.39

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.
data/lib/bluepill.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+
3
+ require 'thread'
4
+ require 'monitor'
5
+ require 'syslog'
6
+ require 'timeout'
7
+ require 'logger'
8
+
9
+ require 'active_support/inflector'
10
+ require 'active_support/core_ext/hash'
11
+ require 'active_support/core_ext/numeric'
12
+ require 'active_support/core_ext/object/misc'
13
+ require 'active_support/duration'
14
+
15
+ require 'bluepill/application'
16
+ require 'bluepill/controller'
17
+ require 'bluepill/socket'
18
+ require "bluepill/process"
19
+ require "bluepill/process_statistics"
20
+ require "bluepill/group"
21
+ require "bluepill/logger"
22
+ require "bluepill/condition_watch"
23
+ require 'bluepill/trigger'
24
+ require 'bluepill/triggers/flapping'
25
+ require "bluepill/dsl"
26
+ require "bluepill/system"
27
+
28
+ require "bluepill/process_conditions"
29
+
30
+ require "bluepill/util/rotational_array"
31
+
32
+ require "bluepill/version"
@@ -0,0 +1,200 @@
1
+ require 'thread'
2
+
3
+ module Bluepill
4
+ class Application
5
+ PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor, :status]
6
+
7
+ attr_accessor :name, :logger, :base_dir, :socket, :pid_file
8
+ attr_accessor :groups, :work_queue
9
+ attr_accessor :pids_dir, :log_file
10
+
11
+ def initialize(name, options = {})
12
+ self.name = name
13
+
14
+ @foreground = options[:foreground]
15
+ self.log_file = options[:log_file]
16
+ self.base_dir = options[:base_dir] || '/var/bluepill'
17
+ self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
18
+ self.pids_dir = File.join(self.base_dir, 'pids', self.name)
19
+
20
+ self.groups = {}
21
+
22
+ self.logger = Bluepill::Logger.new(:log_file => self.log_file, :stdout => foreground?).prefix_with(self.name)
23
+
24
+ self.setup_signal_traps
25
+ self.setup_pids_dir
26
+
27
+ @mutex = Mutex.new
28
+ end
29
+
30
+ def foreground?
31
+ !!@foreground
32
+ end
33
+
34
+ def mutex(&b)
35
+ @mutex.synchronize(&b)
36
+ end
37
+
38
+ def load
39
+ begin
40
+ self.start_server
41
+ rescue StandardError => e
42
+ $stderr.puts "Failed to start bluepill:"
43
+ $stderr.puts "%s `%s`" % [e.class.name, e.message]
44
+ $stderr.puts e.backtrace
45
+ exit(5)
46
+ end
47
+ end
48
+
49
+ PROCESS_COMMANDS.each do |command|
50
+ class_eval <<-END
51
+ def #{command}(group_name = nil, process_name = nil)
52
+ self.send_to_process_or_group(:#{command}, group_name, process_name)
53
+ end
54
+ END
55
+ end
56
+
57
+ def add_process(process, group_name = nil)
58
+ group_name = group_name.to_s if group_name
59
+
60
+ self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name))
61
+ self.groups[group_name].add_process(process)
62
+ end
63
+
64
+ def version
65
+ Bluepill::VERSION
66
+ end
67
+
68
+ protected
69
+ def send_to_process_or_group(method, group_name, process_name)
70
+ if group_name.nil? && process_name.nil?
71
+ self.groups.values.collect do |group|
72
+ group.send(method)
73
+ end.flatten
74
+ elsif self.groups.key?(group_name)
75
+ self.groups[group_name].send(method, process_name)
76
+ elsif process_name.nil?
77
+ # they must be targeting just by process name
78
+ process_name = group_name
79
+ self.groups.values.collect do |group|
80
+ group.send(method, process_name)
81
+ end.flatten
82
+ else
83
+ []
84
+ end
85
+ end
86
+
87
+ def start_listener
88
+ @listener_thread.kill if @listener_thread
89
+ @listener_thread = Thread.new do
90
+ loop do
91
+ begin
92
+ client = self.socket.accept
93
+ command, *args = client.readline.strip.split(":")
94
+ response = begin
95
+ mutex { self.send(command, *args) }
96
+ rescue Exception => e
97
+ e
98
+ end
99
+ client.write(Marshal.dump(response))
100
+ rescue StandardError => e
101
+ logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message])
102
+ e.backtrace.each {|l| logger.err(l)}
103
+ ensure
104
+ begin
105
+ client.close
106
+ rescue IOError
107
+ # closed stream
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def start_server
115
+ self.kill_previous_bluepill
116
+
117
+ Daemonize.daemonize unless foreground?
118
+
119
+ self.logger.reopen
120
+
121
+ $0 = "bluepilld: #{self.name}"
122
+
123
+ self.groups.each {|_, group| group.determine_initial_state }
124
+
125
+
126
+ self.write_pid_file
127
+ self.socket = Bluepill::Socket.server(self.base_dir, self.name)
128
+ self.start_listener
129
+
130
+ self.run
131
+ end
132
+
133
+ def run
134
+ @running = true # set to false by signal trap
135
+ while @running
136
+ mutex do
137
+ System.reset_data
138
+ self.groups.each { |_, group| group.tick }
139
+ end
140
+ sleep 1
141
+ end
142
+ cleanup
143
+ end
144
+
145
+ def cleanup
146
+ File.unlink(self.socket.path) if self.socket
147
+ File.unlink(self.pid_file) if File.exists?(self.pid_file)
148
+ end
149
+
150
+ def setup_signal_traps
151
+ terminator = lambda do
152
+ puts "Terminating..."
153
+ @running = false
154
+ end
155
+
156
+ Signal.trap("TERM", &terminator)
157
+ Signal.trap("INT", &terminator)
158
+
159
+ Signal.trap("HUP") do
160
+ self.logger.reopen if self.logger
161
+ end
162
+ end
163
+
164
+ def setup_pids_dir
165
+ FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir)
166
+ # we need everybody to be able to write to the pids_dir as processes managed by
167
+ # bluepill will be writing to this dir after they've dropped privileges
168
+ FileUtils.chmod(0777, self.pids_dir)
169
+ end
170
+
171
+ def kill_previous_bluepill
172
+ if File.exists?(self.pid_file)
173
+ previous_pid = File.read(self.pid_file).to_i
174
+ begin
175
+ ::Process.kill(0, previous_pid)
176
+ puts "Killing previous bluepilld[#{previous_pid}]"
177
+ ::Process.kill(2, previous_pid)
178
+ rescue Exception => e
179
+ $stderr.puts "Encountered error trying to kill previous bluepill:"
180
+ $stderr.puts "#{e.class}: #{e.message}"
181
+ exit(4) unless e.is_a?(Errno::ESRCH)
182
+ else
183
+ 10.times do |i|
184
+ sleep 0.5
185
+ break unless System.pid_alive?(previous_pid)
186
+ end
187
+
188
+ if System.pid_alive?(previous_pid)
189
+ $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
190
+ exit(4)
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ def write_pid_file
197
+ File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
198
+ end
199
+ end
200
+ 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,55 @@
1
+ module Bluepill
2
+ class ConditionWatch
3
+ attr_accessor :logger, :name
4
+ EMPTY_ARRAY = [].freeze # no need to recreate one every tick
5
+
6
+ def initialize(name, options = {})
7
+ @name = name
8
+
9
+ @logger = options.delete(:logger)
10
+ @fires = options.has_key?(:fires) ? Array(options.delete(:fires)) : [:restart]
11
+ @every = options.delete(:every)
12
+ @times = options.delete(:times) || [1,1]
13
+ @times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
14
+
15
+ self.clear_history!
16
+
17
+ @process_condition = ProcessConditions[@name].new(options)
18
+ end
19
+
20
+ def run(pid, tick_number = Time.now.to_i)
21
+ if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
22
+ @last_ran_at = tick_number
23
+ self.record_value(@process_condition.run(pid))
24
+ return @fires if self.fired?
25
+ end
26
+ EMPTY_ARRAY
27
+ end
28
+
29
+ def record_value(value)
30
+ # TODO: record value in ProcessStatistics
31
+ @history[@history_index] = [value, @process_condition.check(value)]
32
+ @history_index = (@history_index + 1) % @history.size
33
+ self.logger.info(self.to_s)
34
+ end
35
+
36
+ def clear_history!
37
+ @last_ran_at = nil
38
+ @history = Array.new(@times[1])
39
+ @history_index = 0
40
+ end
41
+
42
+ def fired?
43
+ @history.select {|v| v && !v[1]}.size >= @times[0]
44
+ end
45
+
46
+ def to_s
47
+ # TODO: this will be out of order because of the way history values are assigned
48
+ # use (@history[(@history_index - 1)..1] + @history[0..(@history_index - 1)]).
49
+ # collect {|v| "#{v[0]}#{v[1] ? '' : '*'}"}.join(", ")
50
+ # but that's gross so... it's gonna be out of order till we figure out a better way to get it in order
51
+ data = @history.collect {|v| "#{@process_condition.format_value(v[0])}#{v[1] ? '' : '*'}" if v}.compact.join(", ")
52
+ "#{@name}: [#{data}]"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,119 @@
1
+ require 'fileutils'
2
+
3
+ module Bluepill
4
+ class Controller
5
+ attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
6
+
7
+ def initialize(options = {})
8
+ self.log_file = options[:log_file]
9
+ self.base_dir = options[:base_dir]
10
+ self.sockets_dir = File.join(base_dir, 'socks')
11
+ self.pids_dir = File.join(base_dir, 'pids')
12
+
13
+ setup_dir_structure
14
+ cleanup_bluepill_directory
15
+ end
16
+
17
+ def running_applications
18
+ Dir[File.join(sockets_dir, "*.sock")].map{|x| File.basename(x, ".sock")}
19
+ end
20
+
21
+ def handle_command(application, command, *args)
22
+ case command.to_sym
23
+ when :status
24
+ puts self.send_to_daemon(application, :status, *args)
25
+ when *Application::PROCESS_COMMANDS
26
+ # these need to be sent to the daemon and the results printed out
27
+ affected = self.send_to_daemon(application, command, *args)
28
+ if affected.empty?
29
+ puts "No processes effected"
30
+ else
31
+ puts "Sent #{command} to:"
32
+ affected.each do |process|
33
+ puts " #{process}"
34
+ end
35
+ end
36
+ when :quit
37
+ pid = pid_for(application)
38
+ if System.pid_alive?(pid)
39
+ ::Process.kill("TERM", pid)
40
+ puts "Killing bluepilld[#{pid}]"
41
+ else
42
+ puts "bluepilld[#{pid}] not running"
43
+ end
44
+ when :log
45
+ log_file_location = self.send_to_daemon(application, :log_file)
46
+ log_file_location = self.log_file if log_file_location.to_s.strip.empty?
47
+
48
+ requested_pattern = args.first
49
+ grep_pattern = self.grep_pattern(application, requested_pattern)
50
+
51
+ tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
52
+ puts "Tailing log for #{requested_pattern}..."
53
+ Kernel.exec(tail)
54
+ else
55
+ $stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
56
+ end
57
+ end
58
+
59
+ def send_to_daemon(application, command, *args)
60
+ begin
61
+ verify_version!(application)
62
+
63
+ command = ([command, *args]).join(":")
64
+ response = Socket.client_command(base_dir, application, command)
65
+ if response.is_a?(Exception)
66
+ $stderr.puts "Received error from server:"
67
+ $stderr.puts response.inspect
68
+ $stderr.puts response.backtrace.join("\n")
69
+ exit(8)
70
+ else
71
+ response
72
+ end
73
+
74
+ rescue Errno::ECONNREFUSED
75
+ abort("Connection Refused: Server is not running")
76
+ end
77
+ end
78
+
79
+ def grep_pattern(application, query = nil)
80
+ pattern = [application, query].compact.join(':')
81
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
82
+ end
83
+ private
84
+
85
+ def cleanup_bluepill_directory
86
+ self.running_applications.each do |app|
87
+ pid = pid_for(app)
88
+ if !pid || !System.pid_alive?(pid)
89
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
90
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
91
+ File.unlink(pid_file) if File.exists?(pid_file)
92
+ File.unlink(sock_file) if File.exists?(sock_file)
93
+ end
94
+ end
95
+ end
96
+
97
+ def pid_for(app)
98
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
99
+ File.exists?(pid_file) && File.read(pid_file).to_i
100
+ end
101
+
102
+ def setup_dir_structure
103
+ [@sockets_dir, @pids_dir].each do |dir|
104
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
105
+ end
106
+ end
107
+
108
+ def verify_version!(application)
109
+ begin
110
+ version = Socket.client_command(base_dir, application, "version")
111
+ if version != Bluepill::VERSION
112
+ abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
113
+ end
114
+ rescue ArgumentError
115
+ abort("The running version of your daemon seems to be out of date.")
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,150 @@
1
+ require 'ostruct'
2
+ module Bluepill
3
+ def self.define_process_condition(name, &block)
4
+ klass = Class.new(ProcessConditions::ProcessCondition, &block)
5
+ ProcessConditions.const_set("#{name.to_s.camelcase}", klass)
6
+ end
7
+
8
+ def self.application(app_name, options = {}, &block)
9
+ app = Application.new(app_name.to_s, options, &block)
10
+
11
+ process_proxy = Class.new do
12
+ attr_reader :attributes, :watches
13
+ def initialize(process_name = nil)
14
+ @name = process_name
15
+ @attributes = {}
16
+ @watches = {}
17
+ end
18
+
19
+ def method_missing(name, *args)
20
+ if args.size == 1 && name.to_s =~ /^(.*)=$/
21
+ @attributes[$1.to_sym] = args.first
22
+ elsif args.empty? && @attributes.key?(name.to_sym)
23
+ @attributes[name.to_sym]
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def checks(name, options = {})
30
+ @watches[name] = options
31
+ end
32
+
33
+ def validate_child_process(child)
34
+ unless child.attributes.has_key?(:stop_command)
35
+ $stderr.puts "Config Error: Invalid child process monitor for #{@name}"
36
+ $stderr.puts "You must specify a stop command to monitor child processes."
37
+ exit(6)
38
+ end
39
+ end
40
+
41
+ def create_child_process_template
42
+ if @child_process_block
43
+ child_proxy = self.class.new
44
+ # Children inherit some properties of the parent
45
+ [:start_grace_time, :stop_grace_time, :restart_grace_time,
46
+ :start_wait_time, :stop_wait_time, :restart_wait_time].each do |attribute|
47
+ child_proxy.send("#{attribute}=", @attributes[attribute]) if @attributes.key?(attribute)
48
+ end
49
+ @child_process_block.call(child_proxy)
50
+ validate_child_process(child_proxy)
51
+ @attributes[:child_process_template] = child_proxy.to_process(nil)
52
+ end
53
+ end
54
+
55
+ def monitor_children(&child_process_block)
56
+ @child_process_block = child_process_block
57
+ @attributes[:monitor_children] = true
58
+ end
59
+
60
+ def to_process(process_name)
61
+ process = Bluepill::Process.new(process_name, @attributes)
62
+ @watches.each do |name, opts|
63
+ if Bluepill::Trigger[name]
64
+ process.add_trigger(name, opts)
65
+ else
66
+ process.add_watch(name, opts)
67
+ end
68
+ end
69
+
70
+ process
71
+ end
72
+ end
73
+
74
+ app_proxy = Class.new do
75
+ @@app = app
76
+ @@process_proxy = process_proxy
77
+ @@process_keys = Hash.new # because I don't want to require Set just for validations
78
+ @@pid_files = Hash.new
79
+ attr_accessor :working_dir, :uid, :gid, :environment
80
+
81
+ def validate_process(process, process_name)
82
+ # validate uniqueness of group:process
83
+ process_key = [process.attributes[:group], process_name].join(":")
84
+ if @@process_keys.key?(process_key)
85
+ $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
86
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
87
+ $stderr.puts
88
+ exit(6)
89
+ else
90
+ @@process_keys[process_key] = 0
91
+ end
92
+
93
+ # validate required attributes
94
+ [:start_command].each do |required_attr|
95
+ if !process.attributes.key?(required_attr)
96
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
97
+ exit(6)
98
+ end
99
+ end
100
+
101
+ # validate uniqueness of pid files
102
+ pid_key = process.pid_file.strip
103
+ if @@pid_files.key?(pid_key)
104
+ $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
105
+ exit(6)
106
+ else
107
+ @@pid_files[pid_key] = 0
108
+ end
109
+ end
110
+
111
+ def process(process_name, &process_block)
112
+ process_proxy = @@process_proxy.new(process_name)
113
+ process_block.call(process_proxy)
114
+ process_proxy.create_child_process_template
115
+
116
+ set_app_wide_attributes(process_proxy)
117
+
118
+ assign_default_pid_file(process_proxy, process_name)
119
+
120
+ validate_process(process_proxy, process_name)
121
+
122
+ group = process_proxy.attributes.delete(:group)
123
+ process = process_proxy.to_process(process_name)
124
+
125
+
126
+
127
+ @@app.add_process(process, group)
128
+ end
129
+
130
+ def set_app_wide_attributes(process_proxy)
131
+ [:working_dir, :uid, :gid, :environment].each do |attribute|
132
+ unless process_proxy.attributes.key?(attribute)
133
+ process_proxy.attributes[attribute] = self.send(attribute)
134
+ end
135
+ end
136
+ end
137
+
138
+ def assign_default_pid_file(process_proxy, process_name)
139
+ unless process_proxy.attributes.key?(:pid_file)
140
+ group_name = process_proxy.attributes["group"]
141
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
142
+ process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
143
+ end
144
+ end
145
+ end
146
+
147
+ yield(app_proxy.new)
148
+ app.load
149
+ end
150
+ end