dylanvaughn-bluepill 0.0.39

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