gvarela-bluepill 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,211 @@
1
+ module Bluepill
2
+ class Application
3
+ PROCESS_COMMANDS = [:start, :stop, :restart, :unmonitor]
4
+
5
+ attr_accessor :name, :logger, :base_dir, :socket, :pid_file
6
+ attr_accessor :groups, :work_queue
7
+ attr_accessor :pids_dir, :log_file
8
+
9
+ def initialize(name, options = {})
10
+ self.name = name
11
+
12
+ self.log_file = options[:log_file]
13
+ self.base_dir = options[:base_dir] || '/var/bluepill'
14
+ self.pid_file = File.join(self.base_dir, 'pids', self.name + ".pid")
15
+ self.pids_dir = File.join(self.base_dir, 'pids', self.name)
16
+
17
+ self.groups = {}
18
+
19
+ self.logger = Bluepill::Logger.new(:log_file => self.log_file).prefix_with(self.name)
20
+
21
+ self.setup_signal_traps
22
+ self.setup_pids_dir
23
+ end
24
+
25
+ def load
26
+ begin
27
+ self.start_server
28
+ rescue StandardError => e
29
+ $stderr.puts "Failed to start bluepill:"
30
+ $stderr.puts "%s `%s`" % [e.class.name, e.message]
31
+ $stderr.puts e.backtrace
32
+ exit(5)
33
+ end
34
+ end
35
+
36
+ def status
37
+ buffer = []
38
+ depth = 0
39
+
40
+ if self.groups.has_key?(nil)
41
+ self.groups[nil].processes.each do |p|
42
+ buffer << "%s%s(pid:%s): %s" % [" " * depth, p.name, p.actual_pid.inspect, p.state]
43
+
44
+ if p.monitor_children?
45
+ depth += 2
46
+ p.children.each do |c|
47
+ buffer << "%s%s: %s" % [" " * depth, c.name, c.state]
48
+ end
49
+ depth -= 2
50
+ end
51
+ end
52
+ end
53
+
54
+ self.groups.each do |group_name, group|
55
+ next if group_name.nil?
56
+
57
+ buffer << "\n#{group_name}"
58
+
59
+ group.processes.each do |p|
60
+ depth += 2
61
+
62
+ buffer << "%s%s(pid:%s): %s" % [" " * depth, p.name, p.actual_pid.inspect, p.state]
63
+
64
+ if p.monitor_children?
65
+ depth += 2
66
+ p.children.each do |c|
67
+ buffer << "%s%s: %s" % [" " * depth, c.name, c.state]
68
+ end
69
+ depth -= 2
70
+ end
71
+
72
+ depth -= 2
73
+ end
74
+ end
75
+
76
+ buffer.join("\n")
77
+ end
78
+
79
+ PROCESS_COMMANDS.each do |command|
80
+ class_eval <<-END
81
+ def #{command}(group_name, process_name = nil)
82
+ self.send_to_process_or_group(:#{command}, group_name, process_name)
83
+ end
84
+ END
85
+ end
86
+
87
+ def add_process(process, group_name = nil)
88
+ group_name = group_name.to_s if group_name
89
+
90
+ self.groups[group_name] ||= Group.new(group_name, :logger => self.logger.prefix_with(group_name))
91
+ self.groups[group_name].add_process(process)
92
+ end
93
+
94
+ protected
95
+ def send_to_process_or_group(method, group_name, process_name)
96
+ if self.groups.key?(group_name)
97
+ self.groups[group_name].send(method, process_name)
98
+ elsif process_name.nil?
99
+ # they must be targeting just by process name
100
+ process_name = group_name
101
+ self.groups.values.collect do |group|
102
+ group.send(method, process_name)
103
+ end.flatten
104
+ else
105
+ []
106
+ end
107
+ end
108
+
109
+ def start_listener
110
+ @listener_thread.kill if @listener_thread
111
+ @listener_thread = Thread.new do
112
+ begin
113
+ loop do
114
+ client = self.socket.accept
115
+ command, *args = client.readline.strip.split(":")
116
+ response = self.send(command, *args)
117
+ client.write(Marshal.dump(response))
118
+ client.close
119
+ end
120
+ rescue StandardError => e
121
+ logger.err("Got exception in cmd listener: %s `%s`" % [e.class.name, e.message])
122
+ e.backtrace.each {|l| logger.err(l)}
123
+ end
124
+ end
125
+ end
126
+
127
+ def start_server
128
+ self.kill_previous_bluepill
129
+
130
+ Daemonize.daemonize
131
+
132
+ self.logger.reopen
133
+
134
+ $0 = "bluepilld: #{self.name}"
135
+
136
+ self.groups.each {|_, group| group.boot }
137
+
138
+
139
+ self.write_pid_file
140
+ self.socket = Bluepill::Socket.server(self.base_dir, self.name)
141
+ self.start_listener
142
+
143
+ self.run
144
+ end
145
+
146
+ def run
147
+ @running = true # set to false by signal trap
148
+ while @running
149
+ System.reset_data
150
+ self.groups.each { |_, group| group.tick }
151
+ sleep 1
152
+ end
153
+ cleanup
154
+ end
155
+
156
+ def cleanup
157
+ File.unlink(self.socket.path) if self.socket
158
+ File.unlink(self.pid_file) if File.exists?(self.pid_file)
159
+ end
160
+
161
+ def setup_signal_traps
162
+ terminator = lambda do
163
+ puts "Terminating..."
164
+ @running = false
165
+ end
166
+
167
+ Signal.trap("TERM", &terminator)
168
+ Signal.trap("INT", &terminator)
169
+
170
+ Signal.trap("HUP") do
171
+ self.logger.reopen if self.logger
172
+ end
173
+ end
174
+
175
+ def setup_pids_dir
176
+ FileUtils.mkdir_p(self.pids_dir) unless File.exists?(self.pids_dir)
177
+ # we need everybody to be able to write to the pids_dir as processes managed by
178
+ # bluepill will be writing to this dir after they've dropped privileges
179
+ FileUtils.chmod(0777, self.pids_dir)
180
+ end
181
+
182
+ def kill_previous_bluepill
183
+ if File.exists?(self.pid_file)
184
+ previous_pid = File.read(self.pid_file).to_i
185
+ begin
186
+ ::Process.kill(0, previous_pid)
187
+ puts "Killing previous bluepilld[#{previous_pid}]"
188
+ ::Process.kill(2, previous_pid)
189
+ rescue Exception => e
190
+ $stderr.puts "Encountered error trying to kill previous bluepill:"
191
+ $stderr.puts "#{e.class}: #{e.message}"
192
+ exit(4) unless e.is_a?(Errno::ESRCH)
193
+ else
194
+ 10.times do |i|
195
+ sleep 0.5
196
+ break unless System.pid_alive?(previous_pid)
197
+ end
198
+
199
+ if System.pid_alive?(previous_pid)
200
+ $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
201
+ exit(4)
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ def write_pid_file
208
+ File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
209
+ end
210
+ end
211
+ 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,111 @@
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 *Application::PROCESS_COMMANDS
24
+ if args.compact.empty?
25
+ $stderr.puts "You must specify a target process or group for the #{command} command."
26
+ exit(8)
27
+ end
28
+ # these need to be sent to the daemon and the results printed out
29
+ affected = self.send_to_daemon(application, command, *args)
30
+ if affected.empty?
31
+ puts "No processes effected"
32
+ else
33
+ puts "Sent #{command} to:"
34
+ affected.each do |process|
35
+ puts " #{process}"
36
+ end
37
+ end
38
+ when :status
39
+ puts self.send_to_daemon(application, :status, *args)
40
+ when :quit
41
+ pid = pid_for(application)
42
+ if System.pid_alive?(pid)
43
+ ::Process.kill("TERM", pid)
44
+ puts "Killing bluepilld[#{pid}]"
45
+ else
46
+ puts "bluepilld[#{pid}] not running"
47
+ end
48
+ when :log
49
+ log_file_location = self.send_to_daemon(application, :log_file)
50
+ log_file_location = self.log_file if log_file_location.to_s.strip.empty?
51
+
52
+ requested_pattern = args.first
53
+ grep_pattern = self.grep_pattern(application, requested_pattern)
54
+
55
+ tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
56
+ puts "Tailing log for #{requested_pattern}..."
57
+ Kernel.exec(tail)
58
+ else
59
+ $stderr.puts "Unknown command `%s` (or application `%s` has not been loaded yet)" % [command, command]
60
+ end
61
+ end
62
+
63
+ def send_to_daemon(application, command, *args)
64
+
65
+ begin
66
+ Timeout::timeout(Socket::TIMEOUT) do
67
+ buffer = ""
68
+ socket = Socket.client(base_dir, application) # Something that should be interrupted if it takes too much time...
69
+ socket.puts(([command] + args).join(":"))
70
+ while line = socket.gets
71
+ buffer << line
72
+ end
73
+ Marshal.load(buffer)
74
+ end
75
+ rescue Timeout::Error
76
+ abort("Socket Timeout: Server may not be responding")
77
+ rescue Errno::ECONNREFUSED
78
+ abort("Connection Refused: Server is not running")
79
+ end
80
+ end
81
+
82
+ def grep_pattern(application, query = nil)
83
+ pattern = [application, query].compact.join(':')
84
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
85
+ end
86
+ private
87
+
88
+ def cleanup_bluepill_directory
89
+ self.running_applications.each do |app|
90
+ pid = pid_for(app)
91
+ if !pid || !System.pid_alive?(pid)
92
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
93
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
94
+ File.unlink(pid_file) if File.exists?(pid_file)
95
+ File.unlink(sock_file) if File.exists?(sock_file)
96
+ end
97
+ end
98
+ end
99
+
100
+ def pid_for(app)
101
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
102
+ File.exists?(pid_file) && File.read(pid_file).to_i
103
+ end
104
+
105
+ def setup_dir_structure
106
+ [@sockets_dir, @pids_dir].each do |dir|
107
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,140 @@
1
+ require 'ostruct'
2
+ module Bluepill
3
+ def self.application(app_name, options = {}, &block)
4
+ app = Application.new(app_name.to_s, options, &block)
5
+
6
+ process_proxy = Class.new do
7
+ attr_reader :attributes, :watches
8
+ def initialize(process_name = nil)
9
+ @name = process_name
10
+ @attributes = {}
11
+ @watches = {}
12
+ end
13
+
14
+ def method_missing(name, *args)
15
+ if args.size == 1 && name.to_s =~ /^(.*)=$/
16
+ @attributes[$1.to_sym] = args.first
17
+ elsif args.empty? && @attributes.key?(name.to_sym)
18
+ @attributes[name.to_sym]
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def checks(name, options = {})
25
+ @watches[name] = options
26
+ end
27
+
28
+ def validate_child_process(child)
29
+ unless child.attributes.has_key?(:stop_command)
30
+ $stderr.puts "Config Error: Invalid child process monitor for #{@name}"
31
+ $stderr.puts "You must specify a stop command to monitor child processes."
32
+ exit(6)
33
+ end
34
+ end
35
+
36
+ def monitor_children(&child_process_block)
37
+ child_proxy = self.class.new
38
+
39
+ # Children inherit some properties of the parent
40
+ child_proxy.start_grace_time = @attributes[:start_grace_time]
41
+ child_proxy.stop_grace_time = @attributes[:stop_grace_time]
42
+ child_proxy.restart_grace_time = @attributes[:restart_grace_time]
43
+
44
+ child_process_block.call(child_proxy)
45
+ validate_child_process(child_proxy)
46
+
47
+ @attributes[:child_process_template] = child_proxy.to_process(nil)
48
+ # @attributes[:child_process_template].freeze
49
+ @attributes[:monitor_children] = true
50
+ end
51
+
52
+ def to_process(process_name)
53
+ process = Bluepill::Process.new(process_name, @attributes)
54
+ @watches.each do |name, opts|
55
+ if Bluepill::Trigger[name]
56
+ process.add_trigger(name, opts)
57
+ else
58
+ process.add_watch(name, opts)
59
+ end
60
+ end
61
+
62
+ process
63
+ end
64
+ end
65
+
66
+ app_proxy = Class.new do
67
+ @@app = app
68
+ @@process_proxy = process_proxy
69
+ @@process_keys = Hash.new # because I don't want to require Set just for validations
70
+ @@pid_files = Hash.new
71
+ attr_accessor :working_dir, :uid, :gid
72
+
73
+ def validate_process(process, process_name)
74
+ # validate uniqueness of group:process
75
+ process_key = [process.attributes[:group], process_name].join(":")
76
+ if @@process_keys.key?(process_key)
77
+ $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
78
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
79
+ $stderr.puts
80
+ exit(6)
81
+ else
82
+ @@process_keys[process_key] = 0
83
+ end
84
+
85
+ # validate required attributes
86
+ [:start_command].each do |required_attr|
87
+ if !process.attributes.key?(required_attr)
88
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
89
+ exit(6)
90
+ end
91
+ end
92
+
93
+ # validate uniqueness of pid files
94
+ pid_key = process.pid_file.strip
95
+ if @@pid_files.key?(pid_key)
96
+ $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
97
+ exit(6)
98
+ else
99
+ @@pid_files[pid_key] = 0
100
+ end
101
+ end
102
+
103
+ def process(process_name, &process_block)
104
+ process_proxy = @@process_proxy.new(process_name)
105
+ process_block.call(process_proxy)
106
+ set_app_wide_attributes(process_proxy)
107
+
108
+ assign_default_pid_file(process_proxy, process_name)
109
+
110
+ validate_process(process_proxy, process_name)
111
+
112
+ group = process_proxy.attributes.delete(:group)
113
+ process = process_proxy.to_process(process_name)
114
+
115
+
116
+
117
+ @@app.add_process(process, group)
118
+ end
119
+
120
+ def set_app_wide_attributes(process_proxy)
121
+ [:working_dir, :uid, :gid].each do |attribute|
122
+ unless process_proxy.attributes.key?(attribute)
123
+ process_proxy.attributes[attribute] = self.send(attribute)
124
+ end
125
+ end
126
+ end
127
+
128
+ def assign_default_pid_file(process_proxy, process_name)
129
+ unless process_proxy.attributes.key?(:pid_file)
130
+ group_name = process_proxy.attributes["group"]
131
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
132
+ process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
133
+ end
134
+ end
135
+ end
136
+
137
+ yield(app_proxy.new)
138
+ app.load
139
+ end
140
+ end
@@ -0,0 +1,40 @@
1
+ module Bluepill
2
+ class Group
3
+ attr_accessor :name, :processes, :logger
4
+ attr_accessor :process_logger
5
+
6
+ def initialize(name, options = {})
7
+ self.name = name
8
+ self.processes = []
9
+ self.logger = options[:logger]
10
+ end
11
+
12
+ def add_process(process)
13
+ process.logger = self.logger.prefix_with(process.name)
14
+ self.processes << process
15
+ end
16
+
17
+ def tick
18
+ self.processes.each do |process|
19
+ process.tick
20
+ end
21
+ end
22
+
23
+ # proxied events
24
+ [:start, :unmonitor, :stop, :restart, :boot].each do |event|
25
+ class_eval <<-END
26
+ def #{event}(process_name = nil)
27
+ threads = []
28
+ affected = []
29
+ self.processes.each do |process|
30
+ next if process_name && process_name != process.name
31
+ affected << [self.name, process.name].join(":")
32
+ threads << Thread.new { process.handle_user_command("#{event}") }
33
+ end
34
+ threads.each { |t| t.join }
35
+ affected
36
+ end
37
+ END
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ module Bluepill
2
+ class Logger
3
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
4
+
5
+ def initialize(options = {})
6
+ @options = options
7
+ @logger = options[:logger] || self.create_logger
8
+ @prefix = options[:prefix]
9
+ @prefixes = {}
10
+ end
11
+
12
+ LOG_METHODS.each do |method|
13
+ eval <<-END
14
+ def #{method}(msg, prefix = [])
15
+ if @logger.is_a?(self.class)
16
+ @logger.#{method}(msg, [@prefix] + prefix)
17
+ else
18
+ prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
19
+ @logger.#{method}("\#{prefix}\#{msg}")
20
+ end
21
+ end
22
+ END
23
+ end
24
+
25
+ def prefix_with(prefix)
26
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
27
+ end
28
+
29
+ def reopen
30
+ if @logger.is_a?(self.class)
31
+ @logger.reopen
32
+ else
33
+ @logger = create_logger
34
+ end
35
+ end
36
+
37
+ protected
38
+ def create_logger
39
+ if @options[:log_file]
40
+ LoggerAdapter.new(@options[:log_file])
41
+ else
42
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
43
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
44
+ end
45
+ end
46
+
47
+ class LoggerAdapter < ::Logger
48
+ LOGGER_EQUIVALENTS =
49
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
50
+
51
+ LOG_METHODS.each do |method|
52
+ next if method == LOGGER_EQUIVALENTS[method]
53
+ alias_method method, LOGGER_EQUIVALENTS[method]
54
+ end
55
+ end
56
+ end
57
+ end