evented_bluepill 0.0.46

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.
@@ -0,0 +1,202 @@
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
+ if System.pid_alive?(previous_pid)
175
+ begin
176
+ ::Process.kill(0, previous_pid)
177
+ puts "Killing previous bluepilld[#{previous_pid}]"
178
+ ::Process.kill(2, previous_pid)
179
+ rescue Exception => e
180
+ $stderr.puts "Encountered error trying to kill previous bluepill:"
181
+ $stderr.puts "#{e.class}: #{e.message}"
182
+ exit(4) unless e.is_a?(Errno::ESRCH)
183
+ else
184
+ 10.times do |i|
185
+ sleep 0.5
186
+ break unless System.pid_alive?(previous_pid)
187
+ end
188
+
189
+ if System.pid_alive?(previous_pid)
190
+ $stderr.puts "Previous bluepilld[#{previous_pid}] didn't die"
191
+ exit(4)
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def write_pid_file
199
+ File.open(self.pid_file, 'w') { |x| x.write(::Process.pid) }
200
+ end
201
+ end
202
+ 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,120 @@
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
+ exit(1)
57
+ end
58
+ end
59
+
60
+ def send_to_daemon(application, command, *args)
61
+ begin
62
+ verify_version!(application)
63
+
64
+ command = ([command, *args]).join(":")
65
+ response = Socket.client_command(base_dir, application, command)
66
+ if response.is_a?(Exception)
67
+ $stderr.puts "Received error from server:"
68
+ $stderr.puts response.inspect
69
+ $stderr.puts response.backtrace.join("\n")
70
+ exit(8)
71
+ else
72
+ response
73
+ end
74
+
75
+ rescue Errno::ECONNREFUSED
76
+ abort("Connection Refused: Server is not running")
77
+ end
78
+ end
79
+
80
+ def grep_pattern(application, query = nil)
81
+ pattern = [application, query].compact.join(':')
82
+ ['\[.*', Regexp.escape(pattern), '.*'].compact.join
83
+ end
84
+ private
85
+
86
+ def cleanup_bluepill_directory
87
+ self.running_applications.each do |app|
88
+ pid = pid_for(app)
89
+ if !pid || !System.pid_alive?(pid)
90
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
91
+ sock_file = File.join(self.sockets_dir, "#{app}.sock")
92
+ File.unlink(pid_file) if File.exists?(pid_file)
93
+ File.unlink(sock_file) if File.exists?(sock_file)
94
+ end
95
+ end
96
+ end
97
+
98
+ def pid_for(app)
99
+ pid_file = File.join(self.pids_dir, "#{app}.pid")
100
+ File.exists?(pid_file) && File.read(pid_file).to_i
101
+ end
102
+
103
+ def setup_dir_structure
104
+ [@sockets_dir, @pids_dir].each do |dir|
105
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
106
+ end
107
+ end
108
+
109
+ def verify_version!(application)
110
+ begin
111
+ version = Socket.client_command(base_dir, application, "version")
112
+ if version != Bluepill::VERSION
113
+ abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::VERSION}")
114
+ end
115
+ rescue ArgumentError
116
+ abort("The running version of your daemon seems to be out of date.")
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,155 @@
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].each do |attribute|
46
+ child_proxy.send("#{attribute}=", @attributes[attribute]) if @attributes.key?(attribute)
47
+ end
48
+ @child_process_block.call(child_proxy)
49
+ validate_child_process(child_proxy)
50
+ @attributes[:child_process_template] = child_proxy.to_process(nil)
51
+ end
52
+ end
53
+
54
+ def monitor_children(&child_process_block)
55
+ @child_process_block = child_process_block
56
+ @attributes[:monitor_children] = true
57
+ end
58
+
59
+ def to_process(process_name)
60
+ process = Bluepill::Process.new(process_name, @attributes)
61
+ @watches.each do |name, opts|
62
+ if Bluepill::Trigger[name]
63
+ process.add_trigger(name, opts)
64
+ else
65
+ process.add_watch(name, opts)
66
+ end
67
+ end
68
+
69
+ process
70
+ end
71
+ end
72
+ app_proxy = Class.new do
73
+ if RUBY_VERSION >= '1.9'
74
+ class_variable_set(:@@app, app)
75
+ class_variable_set(:@@process_proxy, process_proxy)
76
+ class_variable_set(:@@process_keys, Hash.new) # because I don't want to require Set just for validations
77
+ class_variable_set(:@@pid_files, Hash.new)
78
+ else
79
+ @@app = app
80
+ @@process_proxy = process_proxy
81
+ @@process_keys = Hash.new
82
+ @@pid_files = Hash.new
83
+ end
84
+ attr_accessor :working_dir, :uid, :gid, :environment
85
+
86
+ def validate_process(process, process_name)
87
+ # validate uniqueness of group:process
88
+ process_key = [process.attributes[:group], process_name].join(":")
89
+ if @@process_keys.key?(process_key)
90
+ $stderr.print "Config Error: You have two entries for the process name '#{process_name}'"
91
+ $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
92
+ $stderr.puts
93
+ exit(6)
94
+ else
95
+ @@process_keys[process_key] = 0
96
+ end
97
+
98
+ # validate required attributes
99
+ [:start_command].each do |required_attr|
100
+ if !process.attributes.key?(required_attr)
101
+ $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process_name}'"
102
+ exit(6)
103
+ end
104
+ end
105
+
106
+ # validate uniqueness of pid files
107
+ pid_key = process.pid_file.strip
108
+ if @@pid_files.key?(pid_key)
109
+ $stderr.puts "Config Error: You have two entries with the pid file: #{process.pid_file}"
110
+ exit(6)
111
+ else
112
+ @@pid_files[pid_key] = 0
113
+ end
114
+ end
115
+
116
+ def process(process_name, &process_block)
117
+ process_proxy = @@process_proxy.new(process_name)
118
+ process_block.call(process_proxy)
119
+ process_proxy.create_child_process_template
120
+
121
+ set_app_wide_attributes(process_proxy)
122
+
123
+ assign_default_pid_file(process_proxy, process_name)
124
+
125
+ validate_process(process_proxy, process_name)
126
+
127
+ group = process_proxy.attributes.delete(:group)
128
+ process = process_proxy.to_process(process_name)
129
+
130
+
131
+
132
+ @@app.add_process(process, group)
133
+ end
134
+
135
+ def set_app_wide_attributes(process_proxy)
136
+ [:working_dir, :uid, :gid, :environment].each do |attribute|
137
+ unless process_proxy.attributes.key?(attribute)
138
+ process_proxy.attributes[attribute] = self.send(attribute)
139
+ end
140
+ end
141
+ end
142
+
143
+ def assign_default_pid_file(process_proxy, process_name)
144
+ unless process_proxy.attributes.key?(:pid_file)
145
+ group_name = process_proxy.attributes["group"]
146
+ default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_\-]/, "_")
147
+ process_proxy.pid_file = File.join(@@app.pids_dir, default_pid_name + ".pid")
148
+ end
149
+ end
150
+ end
151
+
152
+ yield(app_proxy.new)
153
+ app.load
154
+ end
155
+ end
@@ -0,0 +1,71 @@
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
+ def determine_initial_state
24
+ self.processes.each do |process|
25
+ process.determine_initial_state
26
+ end
27
+ end
28
+
29
+ # proxied events
30
+ [:start, :unmonitor, :stop, :restart].each do |event|
31
+ class_eval <<-END
32
+ def #{event}(process_name = nil)
33
+ threads = []
34
+ affected = []
35
+ self.processes.each do |process|
36
+ next if process_name && process_name != process.name
37
+ affected << [self.name, process.name].join(":")
38
+ threads << Thread.new { process.handle_user_command("#{event}") }
39
+ end
40
+ threads.each { |t| t.join }
41
+ affected
42
+ end
43
+ END
44
+ end
45
+
46
+ def status(process_name = nil)
47
+ lines = []
48
+ if process_name.nil?
49
+ prefix = self.name ? " " : ""
50
+ lines << "#{self.name}:" if self.name
51
+
52
+ self.processes.each do |process|
53
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
54
+ if process.monitor_children?
55
+ process.children.each do |child|
56
+ lines << " %s%s: %s" % [prefix, child.name, child.state]
57
+ end
58
+ end
59
+ end
60
+ else
61
+ self.processes.each do |process|
62
+ next if process_name != process.name
63
+ lines << "%s%s(pid:%s): %s" % [prefix, process.name, process.actual_pid, process.state]
64
+ lines << process.statistics.to_s
65
+ end
66
+ end
67
+ lines << ""
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,62 @@
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
+ @stdout = options[:stdout]
10
+ @prefixes = {}
11
+ end
12
+
13
+ LOG_METHODS.each do |method|
14
+ eval <<-END
15
+ def #{method}(msg, prefix = [])
16
+ if @logger.is_a?(self.class)
17
+ @logger.#{method}(msg, [@prefix] + prefix)
18
+ else
19
+ s_prefix = prefix.size > 0 ? "[\#{prefix.compact.join(':')}] " : ""
20
+ if @stdout
21
+ $stdout.puts("[#{method}]: \#{s_prefix}\#{msg}")
22
+ $stdout.flush
23
+ end
24
+ @logger.#{method}("\#{s_prefix}\#{msg}")
25
+ end
26
+ end
27
+ END
28
+ end
29
+
30
+ def prefix_with(prefix)
31
+ @prefixes[prefix] ||= self.class.new(:logger => self, :prefix => prefix)
32
+ end
33
+
34
+ def reopen
35
+ if @logger.is_a?(self.class)
36
+ @logger.reopen
37
+ else
38
+ @logger = create_logger
39
+ end
40
+ end
41
+
42
+ protected
43
+ def create_logger
44
+ if @options[:log_file]
45
+ LoggerAdapter.new(@options[:log_file])
46
+ else
47
+ Syslog.close if Syslog.opened? # need to explictly close it before reopening it
48
+ Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
49
+ end
50
+ end
51
+
52
+ class LoggerAdapter < ::Logger
53
+ LOGGER_EQUIVALENTS =
54
+ {:debug => :debug, :err => :error, :warning => :warn, :info => :info, :emerg => :fatal, :alert => :warn, :crit => :fatal, :notice => :info}
55
+
56
+ LOG_METHODS.each do |method|
57
+ next if method == LOGGER_EQUIVALENTS[method]
58
+ alias_method method, LOGGER_EQUIVALENTS[method]
59
+ end
60
+ end
61
+ end
62
+ end