bluepill 0.1.3 → 0.1.4
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +820 -0
- data/CONTRIBUTING.md +31 -0
- data/LICENSE +1 -1
- data/README.md +64 -29
- data/bin/bluepill +1 -1
- data/bluepill.gemspec +17 -6
- data/lib/bluepill/application.rb +309 -41
- data/lib/bluepill/condition_watch.rb +103 -8
- data/lib/bluepill/controller.rb +124 -13
- data/lib/bluepill/dsl/app_proxy.rb +102 -3
- data/lib/bluepill/dsl/process_factory.rb +183 -60
- data/lib/bluepill/dsl/process_proxy.rb +132 -14
- data/lib/bluepill/dsl.rb +27 -5
- data/lib/bluepill/group.rb +142 -9
- data/lib/bluepill/logger.rb +120 -5
- data/lib/bluepill/process.rb +571 -200
- data/lib/bluepill/process_commands.rb +262 -0
- data/lib/bluepill/process_conditions/always_true.rb +18 -4
- data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
- data/lib/bluepill/process_conditions/file_time.rb +40 -4
- data/lib/bluepill/process_conditions/http.rb +81 -23
- data/lib/bluepill/process_conditions/mem_usage.rb +61 -2
- data/lib/bluepill/process_conditions/process_condition.rb +43 -0
- data/lib/bluepill/process_conditions/running_time.rb +36 -1
- data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
- data/lib/bluepill/process_conditions.rb +15 -1
- data/lib/bluepill/process_journal.rb +256 -104
- data/lib/bluepill/process_statistics.rb +33 -3
- data/lib/bluepill/socket.rb +63 -16
- data/lib/bluepill/system/commands.rb +286 -0
- data/lib/bluepill/system.rb +192 -228
- data/lib/bluepill/trigger.rb +110 -14
- data/lib/bluepill/triggers/flapping.rb +84 -3
- data/lib/bluepill/util/rotational_array.rb +24 -1
- data/lib/bluepill/version.rb +71 -19
- data/lib/bluepill.rb +25 -2
- data/sig/bluepill.rbs +926 -0
- data/sig/manifest.yaml +7 -0
- metadata +16 -34
- data/bin/sample_forking_server +0 -66
- data/lib/bluepill/application/client.rb +0 -6
- data/lib/bluepill/application/server.rb +0 -21
data/lib/bluepill/controller.rb
CHANGED
|
@@ -2,12 +2,56 @@ require 'fileutils'
|
|
|
2
2
|
require 'bluepill/system'
|
|
3
3
|
|
|
4
4
|
module Bluepill
|
|
5
|
+
# Run the bluepill CLI's commands, and send them to the daemon
|
|
6
|
+
#
|
|
7
|
+
# The CLI and the daemon talk over a socket of the application's in the
|
|
8
|
+
# base directory.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
5
11
|
class Controller
|
|
6
12
|
attr_accessor :base_dir, :log_file, :sockets_dir, :pids_dir
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
# @!attribute [rw] base_dir
|
|
15
|
+
# The directory with the daemons' sockets and pid files
|
|
16
|
+
#
|
|
17
|
+
# @return [String]
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] log_file
|
|
22
|
+
# The log file to tail when the daemon doesn't name one
|
|
23
|
+
#
|
|
24
|
+
# @return [String, nil]
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] sockets_dir
|
|
29
|
+
# The directory with the daemons' sockets
|
|
30
|
+
#
|
|
31
|
+
# @return [String]
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
|
|
35
|
+
# @!attribute [rw] pids_dir
|
|
36
|
+
# The directory with the daemons' pid files
|
|
37
|
+
#
|
|
38
|
+
# @return [String]
|
|
39
|
+
#
|
|
40
|
+
# @api private
|
|
41
|
+
|
|
42
|
+
# Create the controller, and clean up after daemons that died
|
|
43
|
+
#
|
|
44
|
+
# It creates the sockets and pids directories, and removes the socket
|
|
45
|
+
# and pid file of each daemon that's no longer running.
|
|
46
|
+
#
|
|
47
|
+
# @param [Hash{Symbol => Object}] options :base_dir, and :log_file
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
def initialize(options)
|
|
9
53
|
self.log_file = options[:log_file]
|
|
10
|
-
self.base_dir = options
|
|
54
|
+
self.base_dir = options.fetch(:base_dir)
|
|
11
55
|
self.sockets_dir = File.join(base_dir, 'socks')
|
|
12
56
|
self.pids_dir = File.join(base_dir, 'pids')
|
|
13
57
|
|
|
@@ -15,17 +59,35 @@ module Bluepill
|
|
|
15
59
|
cleanup_bluepill_directory
|
|
16
60
|
end
|
|
17
61
|
|
|
62
|
+
# Return the names of the applications with a daemon's socket
|
|
63
|
+
#
|
|
64
|
+
# @return [Array<String>]
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
18
67
|
def running_applications
|
|
19
68
|
Dir[File.join(sockets_dir, '*.sock')].collect { |x| File.basename(x, '.sock') }
|
|
20
69
|
end
|
|
21
70
|
|
|
71
|
+
# Run a CLI command and print what it did
|
|
72
|
+
#
|
|
73
|
+
# status prints the daemon's status, quit stops the daemon, log tails
|
|
74
|
+
# the log for a process or group, and the PROCESS_COMMANDS go to the
|
|
75
|
+
# daemon. It exits with status 1 for an unknown command.
|
|
76
|
+
#
|
|
77
|
+
# @param [String] application the application's name
|
|
78
|
+
# @param [String, Symbol] command the command
|
|
79
|
+
# @param [Array<String>] args the group or process, for example
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
22
84
|
def handle_command(application, command, *args)
|
|
23
85
|
case command.to_sym
|
|
24
86
|
when :status
|
|
25
87
|
puts send_to_daemon(application, :status, *args)
|
|
26
88
|
when :quit
|
|
27
89
|
pid = pid_for(application)
|
|
28
|
-
if System.pid_alive?(pid)
|
|
90
|
+
if pid && System.pid_alive?(pid)
|
|
29
91
|
::Process.kill('TERM', pid)
|
|
30
92
|
puts "Killing bluepilld[#{pid}]"
|
|
31
93
|
else
|
|
@@ -33,12 +95,12 @@ module Bluepill
|
|
|
33
95
|
end
|
|
34
96
|
when :log
|
|
35
97
|
log_file_location = send_to_daemon(application, :log_file)
|
|
36
|
-
log_file_location = log_file if log_file_location.to_s.
|
|
98
|
+
log_file_location = log_file if log_file_location.to_s.rstrip.empty?
|
|
37
99
|
|
|
38
100
|
requested_pattern = args.first
|
|
39
|
-
|
|
101
|
+
pattern = grep_pattern(application, requested_pattern)
|
|
40
102
|
|
|
41
|
-
tail = "tail -n 100 -f #{log_file_location} | grep -E '#{
|
|
103
|
+
tail = "tail -n 100 -f #{log_file_location} | grep -E '#{pattern}'"
|
|
42
104
|
puts "Tailing log for #{requested_pattern}..."
|
|
43
105
|
Kernel.exec(tail)
|
|
44
106
|
when *Application::PROCESS_COMMANDS
|
|
@@ -58,6 +120,19 @@ module Bluepill
|
|
|
58
120
|
end
|
|
59
121
|
end
|
|
60
122
|
|
|
123
|
+
# Send a command to an application's daemon, and return its answer
|
|
124
|
+
#
|
|
125
|
+
# It first checks that the daemon runs this version of bluepill. It
|
|
126
|
+
# prints an error that the daemon raised and exits with status 8, and
|
|
127
|
+
# aborts if the daemon isn't running.
|
|
128
|
+
#
|
|
129
|
+
# @param [String] application the application's name
|
|
130
|
+
# @param [String, Symbol] command the command
|
|
131
|
+
# @param [Array<String>] args the command's arguments
|
|
132
|
+
#
|
|
133
|
+
# @return [Object] what the daemon answered
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
61
136
|
def send_to_daemon(application, command, *args)
|
|
62
137
|
verify_version!(application)
|
|
63
138
|
|
|
@@ -66,27 +141,41 @@ module Bluepill
|
|
|
66
141
|
if response.is_a?(Exception)
|
|
67
142
|
$stderr.puts 'Received error from server:'
|
|
68
143
|
$stderr.puts response.inspect
|
|
69
|
-
|
|
144
|
+
# join flattens the backtrace and makes a missing one empty
|
|
145
|
+
$stderr.puts [response.backtrace].join("\n")
|
|
70
146
|
exit(8)
|
|
71
147
|
else
|
|
72
148
|
response
|
|
73
149
|
end
|
|
74
|
-
|
|
75
150
|
rescue Errno::ECONNREFUSED
|
|
76
151
|
abort('Connection Refused: Server is not running')
|
|
77
152
|
end
|
|
78
153
|
|
|
154
|
+
# Return the pattern that finds a process or group's lines in the log
|
|
155
|
+
#
|
|
156
|
+
# @param [String] application the application's name
|
|
157
|
+
# @param [String, nil] query the process or group
|
|
158
|
+
#
|
|
159
|
+
# @return [String] an extended regular expression for grep
|
|
160
|
+
#
|
|
161
|
+
# @api private
|
|
79
162
|
def grep_pattern(application, query = nil)
|
|
80
163
|
pattern = [application, query].compact.join(':')
|
|
81
|
-
['\[.*', Regexp.escape(pattern), '.*'].
|
|
164
|
+
['\[.*', Regexp.escape(pattern), '.*'].join
|
|
82
165
|
end
|
|
83
166
|
|
|
84
167
|
private
|
|
85
168
|
|
|
169
|
+
# Remove the socket and pid file of each daemon that's not running
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
#
|
|
173
|
+
# @api private
|
|
86
174
|
def cleanup_bluepill_directory
|
|
87
175
|
running_applications.each do |app|
|
|
88
176
|
pid = pid_for(app)
|
|
89
177
|
next if pid && System.pid_alive?(pid)
|
|
178
|
+
|
|
90
179
|
pid_file = File.join(pids_dir, "#{app}.pid")
|
|
91
180
|
sock_file = File.join(sockets_dir, "#{app}.sock")
|
|
92
181
|
System.delete_if_exists(pid_file)
|
|
@@ -94,22 +183,44 @@ module Bluepill
|
|
|
94
183
|
end
|
|
95
184
|
end
|
|
96
185
|
|
|
186
|
+
# Return the pid in an application's pid file
|
|
187
|
+
#
|
|
188
|
+
# @param [String] app the application's name
|
|
189
|
+
#
|
|
190
|
+
# @return [Integer, false] the pid, or false without a valid one
|
|
191
|
+
#
|
|
192
|
+
# @api private
|
|
97
193
|
def pid_for(app)
|
|
98
194
|
pid_file = File.join(pids_dir, "#{app}.pid")
|
|
99
|
-
File.exist?(pid_file)
|
|
195
|
+
return false unless File.exist?(pid_file)
|
|
196
|
+
|
|
197
|
+
# An empty or garbled pid file reads as 0, and signaling pid 0 would
|
|
198
|
+
# hit the CLI's own process group
|
|
199
|
+
pid = File.read(pid_file).to_i
|
|
200
|
+
pid > 0 ? pid : false
|
|
100
201
|
end
|
|
101
202
|
|
|
203
|
+
# Create the sockets and pids directories
|
|
204
|
+
#
|
|
205
|
+
# @return [void]
|
|
206
|
+
#
|
|
207
|
+
# @api private
|
|
102
208
|
def setup_dir_structure
|
|
103
209
|
[@sockets_dir, @pids_dir].each do |dir|
|
|
104
210
|
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
|
105
211
|
end
|
|
106
212
|
end
|
|
107
213
|
|
|
214
|
+
# Abort unless an application's daemon runs this version of bluepill
|
|
215
|
+
#
|
|
216
|
+
# @param [String] application the application's name
|
|
217
|
+
#
|
|
218
|
+
# @return [void]
|
|
219
|
+
#
|
|
220
|
+
# @api private
|
|
108
221
|
def verify_version!(application)
|
|
109
222
|
version = Socket.client_command(base_dir, application, 'version')
|
|
110
|
-
|
|
111
|
-
abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Bluepill::Version}")
|
|
112
|
-
end
|
|
223
|
+
abort("The running version of your daemon seems to be out of date.\nDaemon Version: #{version}, CLI Version: #{Version}") unless version.equal?(Version)
|
|
113
224
|
rescue ArgumentError
|
|
114
225
|
abort('The running version of your daemon seems to be out of date.')
|
|
115
226
|
end
|
|
@@ -1,17 +1,116 @@
|
|
|
1
1
|
module Bluepill
|
|
2
|
+
# The self of the block given to {Bluepill.application}
|
|
3
|
+
#
|
|
4
|
+
# It sets defaults that every process of the application gets, and defines
|
|
5
|
+
# the processes.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# Bluepill.application(:my_app) do |app|
|
|
9
|
+
# app.working_dir = '/srv/my_app'
|
|
10
|
+
# app.process('web') { |process| process.start_command = 'bin/server' }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
2
14
|
class AppProxy
|
|
15
|
+
# The attributes that every process of the application gets, unless the
|
|
16
|
+
# process sets its own
|
|
3
17
|
APP_ATTRIBUTES = [:working_dir, :uid, :gid, :environment, :auto_start].freeze
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
# The directory that the application's processes start in
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# app.working_dir = '/srv/my_app'
|
|
23
|
+
#
|
|
24
|
+
# @return [String, nil] the directory, or nil for the default
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
attr_accessor :working_dir
|
|
28
|
+
|
|
29
|
+
# The user that the application's processes run as
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# app.uid = 'deploy'
|
|
33
|
+
#
|
|
34
|
+
# @return [String, nil] the user's name, or nil for bluepill's own user
|
|
35
|
+
#
|
|
36
|
+
# @api public
|
|
37
|
+
attr_accessor :uid
|
|
38
|
+
|
|
39
|
+
# The group that the application's processes run as
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# app.gid = 'deploy'
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil] the group's name, or nil for bluepill's own group
|
|
45
|
+
#
|
|
46
|
+
# @api public
|
|
47
|
+
attr_accessor :gid
|
|
48
|
+
|
|
49
|
+
# The environment variables that the application's processes get
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# app.environment = {'RAILS_ENV' => 'production'}
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash{#to_s => #to_s}, nil] the variables by name
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
attr_accessor :environment
|
|
58
|
+
|
|
59
|
+
# Whether the application's processes start when bluepill loads
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# app.auto_start = false
|
|
63
|
+
#
|
|
64
|
+
# @return [Boolean, nil] false to leave the processes unmonitored until
|
|
65
|
+
# they're started
|
|
66
|
+
#
|
|
67
|
+
# @api public
|
|
68
|
+
attr_accessor :auto_start
|
|
69
|
+
|
|
70
|
+
# Return the application that the config describes
|
|
71
|
+
#
|
|
72
|
+
# @return [Application] the application
|
|
73
|
+
#
|
|
74
|
+
# @api private
|
|
6
75
|
attr_reader :app
|
|
7
76
|
|
|
77
|
+
# Create the proxy of a new application
|
|
78
|
+
#
|
|
79
|
+
# @param [#to_s] app_name the application's name
|
|
80
|
+
# @param [Hash{Symbol => Object}] options the application's options
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
8
85
|
def initialize(app_name, options)
|
|
9
86
|
@app = Application.new(app_name.to_s, options)
|
|
10
87
|
end
|
|
11
88
|
|
|
89
|
+
# Define a process of the application
|
|
90
|
+
#
|
|
91
|
+
# The block runs with self set to a {ProcessProxy}, and also gets the
|
|
92
|
+
# proxy as an argument. The process starts with the application's
|
|
93
|
+
# attributes, which the block can override.
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# app.process('web') do |process|
|
|
97
|
+
# process.start_command = '/usr/bin/my_server'
|
|
98
|
+
# process.group = 'servers'
|
|
99
|
+
# end
|
|
100
|
+
#
|
|
101
|
+
# @param [String] process_name the process's name
|
|
102
|
+
#
|
|
103
|
+
# @yield [process] configures the process
|
|
104
|
+
# @yieldparam [ProcessProxy] process the process's proxy, which is also
|
|
105
|
+
# self
|
|
106
|
+
# @yieldreturn [void]
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Process>] the processes of the process's group
|
|
109
|
+
#
|
|
110
|
+
# @api public
|
|
12
111
|
def process(process_name, &process_block)
|
|
13
|
-
attributes = {}
|
|
14
|
-
APP_ATTRIBUTES.each { |a| attributes[a] =
|
|
112
|
+
attributes = {} #: Hash[Symbol, untyped]
|
|
113
|
+
APP_ATTRIBUTES.each { |a| attributes[a] = public_send(a) }
|
|
15
114
|
|
|
16
115
|
process_factory = ProcessFactory.new(attributes, process_block)
|
|
17
116
|
|
|
@@ -1,29 +1,78 @@
|
|
|
1
1
|
module Bluepill
|
|
2
|
+
# Create the processes that a config file describes, and check them
|
|
3
|
+
#
|
|
4
|
+
# A factory turns a process block into a {Process}, after it checks the
|
|
5
|
+
# config for mistakes, such as two processes with the same name or pid
|
|
6
|
+
# file. A process that monitors its children gets a factory of its own for
|
|
7
|
+
# them.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
2
10
|
class ProcessFactory
|
|
11
|
+
# Return the attributes that each process starts with
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash{Symbol => Object}] the attributes
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
3
16
|
attr_reader :attributes
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
@@
|
|
7
|
-
|
|
18
|
+
# Only the keys matter
|
|
19
|
+
@@process_keys = {} #: Hash[Array[String?], nil]
|
|
20
|
+
@@pid_files = {} #: Hash[String, nil]
|
|
21
|
+
|
|
22
|
+
# Create a factory of processes
|
|
23
|
+
#
|
|
24
|
+
# @param [Hash{Symbol => Object}] attributes the attributes that each
|
|
25
|
+
# process starts with
|
|
26
|
+
# @param [Proc] process_block the block that configures each process
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
8
31
|
def initialize(attributes, process_block)
|
|
9
32
|
@attributes = attributes
|
|
10
33
|
@process_block = process_block
|
|
11
34
|
end
|
|
12
35
|
|
|
36
|
+
# Create a process from the factory's block, and check its config
|
|
37
|
+
#
|
|
38
|
+
# A process without a pid_file gets a default one in pids_dir.
|
|
39
|
+
#
|
|
40
|
+
# @param [String] name the process's name
|
|
41
|
+
# @param [String] pids_dir the directory of the default pid file
|
|
42
|
+
#
|
|
43
|
+
# @return [Process] the process
|
|
44
|
+
#
|
|
45
|
+
# @api private
|
|
13
46
|
def create_process(name, pids_dir)
|
|
14
|
-
assign_default_pid_file(name, pids_dir)
|
|
47
|
+
default_pid_file = assign_default_pid_file(name, pids_dir)
|
|
15
48
|
|
|
16
49
|
process = ProcessProxy.new(name, @attributes, @process_block)
|
|
17
50
|
child_process_block = @attributes.delete(:child_process_block)
|
|
18
51
|
@attributes[:child_process_factory] = ProcessFactory.new(@attributes, child_process_block) if @attributes[:monitor_children]
|
|
19
52
|
|
|
20
53
|
validate_process! process
|
|
54
|
+
warn_unless_pid_known(process, default_pid_file)
|
|
21
55
|
process.to_process
|
|
22
56
|
end
|
|
23
57
|
|
|
58
|
+
# Create the process of a child that bluepill found
|
|
59
|
+
#
|
|
60
|
+
# The child gets its parent's grace times, and its state from whether
|
|
61
|
+
# it's running.
|
|
62
|
+
#
|
|
63
|
+
# @param [String] name the child's name
|
|
64
|
+
# @param [Integer] pid the child's pid
|
|
65
|
+
# @param [Logger] logger the child's logger
|
|
66
|
+
#
|
|
67
|
+
# @return [Process] the child's process
|
|
68
|
+
#
|
|
69
|
+
# @api private
|
|
24
70
|
def create_child_process(name, pid, logger)
|
|
25
|
-
attributes = {}
|
|
26
|
-
|
|
71
|
+
attributes = {} #: Hash[Symbol, untyped]
|
|
72
|
+
# A grace time the parent leaves unset keeps the Process default
|
|
73
|
+
[:start_grace_time, :stop_grace_time, :restart_grace_time].each do |a|
|
|
74
|
+
attributes[a] = @attributes.fetch(a) if @attributes.key?(a)
|
|
75
|
+
end
|
|
27
76
|
attributes[:actual_pid] = pid
|
|
28
77
|
attributes[:logger] = logger
|
|
29
78
|
|
|
@@ -37,79 +86,153 @@ module Bluepill
|
|
|
37
86
|
|
|
38
87
|
protected
|
|
39
88
|
|
|
89
|
+
# Set the default pid file, unless the process sets its own
|
|
90
|
+
#
|
|
91
|
+
# @param [String] process_name the process's name
|
|
92
|
+
# @param [String] pids_dir the directory of the pid file
|
|
93
|
+
#
|
|
94
|
+
# @return [String, nil] the default pid file, or nil when the process
|
|
95
|
+
# sets its own
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
40
98
|
def assign_default_pid_file(process_name, pids_dir)
|
|
41
99
|
return if @attributes.key?(:pid_file)
|
|
100
|
+
|
|
42
101
|
group_name = @attributes[:group]
|
|
43
|
-
default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_
|
|
44
|
-
@attributes[:pid_file] = File.join(pids_dir, default_pid_name
|
|
102
|
+
default_pid_name = [group_name, process_name].compact.join('_').gsub(/[^A-Za-z0-9_-]/, '_')
|
|
103
|
+
@attributes[:pid_file] = File.join(pids_dir, "#{default_pid_name}.pid")
|
|
45
104
|
end
|
|
46
105
|
|
|
106
|
+
# Check a process's config, and exit on a mistake
|
|
107
|
+
#
|
|
108
|
+
# The process shares its attributes hash with the factory.
|
|
109
|
+
#
|
|
110
|
+
# @param [ProcessProxy] process the process's proxy
|
|
111
|
+
#
|
|
112
|
+
# @return [void]
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
47
115
|
def validate_process!(process)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
$stderr.puts
|
|
54
|
-
exit(6)
|
|
55
|
-
else
|
|
56
|
-
@@process_keys[process_key] = 0
|
|
57
|
-
end
|
|
116
|
+
validate_unique_process!(process)
|
|
117
|
+
validate_required_attributes!(process)
|
|
118
|
+
validate_unique_pid_file!
|
|
119
|
+
validate_stop_signals!
|
|
120
|
+
end
|
|
58
121
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
# Exit if another process in the group has the same name
|
|
123
|
+
#
|
|
124
|
+
# @param [ProcessProxy] process the process's proxy
|
|
125
|
+
#
|
|
126
|
+
# @return [void]
|
|
127
|
+
#
|
|
128
|
+
# @api private
|
|
129
|
+
def validate_unique_process!(process)
|
|
130
|
+
# A process name may contain ':', so the key keeps the parts apart.
|
|
131
|
+
# Like Application#add_process, it treats Symbols and Strings alike.
|
|
132
|
+
process_key = [attributes[:group], process.name].collect { |part| part && part.to_s }
|
|
133
|
+
if @@process_keys.key?(process_key)
|
|
134
|
+
message = "Config Error: You have two entries for the process name '#{process.name}'"
|
|
135
|
+
message += " in the group '#{attributes.fetch(:group)}'" if attributes.key?(:group)
|
|
136
|
+
config_error!(message)
|
|
65
137
|
end
|
|
138
|
+
@@process_keys[process_key] = nil
|
|
139
|
+
end
|
|
66
140
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
141
|
+
# Exit unless the process has a start_command
|
|
142
|
+
#
|
|
143
|
+
# @param [ProcessProxy] process the process's proxy
|
|
144
|
+
#
|
|
145
|
+
# @return [void]
|
|
146
|
+
#
|
|
147
|
+
# @api private
|
|
148
|
+
def validate_required_attributes!(process)
|
|
149
|
+
return if attributes.key?(:start_command)
|
|
150
|
+
|
|
151
|
+
config_error!("Config Error: You must specify a start_command for '#{process.name}'")
|
|
152
|
+
end
|
|
75
153
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
154
|
+
# Exit if another process has the same pid file
|
|
155
|
+
#
|
|
156
|
+
# @return [void]
|
|
157
|
+
#
|
|
158
|
+
# @api private
|
|
159
|
+
def validate_unique_pid_file!
|
|
160
|
+
pid_key = attributes.fetch(:pid_file).strip
|
|
161
|
+
config_error!("Config Error: You have two entries with the pid file: #{pid_key}") if @@pid_files.key?(pid_key)
|
|
162
|
+
@@pid_files[pid_key] = nil
|
|
163
|
+
end
|
|
79
164
|
|
|
165
|
+
# Exit unless stop_signals alternates signals and delays
|
|
166
|
+
#
|
|
167
|
+
# The signals and delays must alternate, starting and ending with a
|
|
168
|
+
# signal, and stop_grace_time must be longer than the delays. Checks the
|
|
169
|
+
# more specific problems before the 'odd number' message.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
#
|
|
173
|
+
# @api private
|
|
174
|
+
def validate_stop_signals!
|
|
175
|
+
stop_signals = attributes[:stop_signals]
|
|
80
176
|
return if stop_signals.nil?
|
|
81
|
-
# Start with the more helpful error messages before the 'odd number' message.
|
|
82
|
-
delay_sum = 0
|
|
83
|
-
stop_signals.each_with_index do |s_or_d, i|
|
|
84
|
-
if i.even?
|
|
85
|
-
signal = s_or_d
|
|
86
|
-
unless signal.is_a? Symbol
|
|
87
|
-
$stderr.puts "Config Error: Invalid stop_signals! Expected a symbol (signal) at position #{i} instead of '#{signal}'."
|
|
88
|
-
exit(6)
|
|
89
|
-
end
|
|
90
|
-
else
|
|
91
|
-
delay = s_or_d
|
|
92
|
-
unless delay.is_a? Fixnum
|
|
93
|
-
$stderr.puts "Config Error: Invalid stop_signals! Expected a number (delay) at position #{i} instead of '#{delay}'."
|
|
94
|
-
exit(6)
|
|
95
|
-
end
|
|
96
|
-
delay_sum += delay
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
177
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end
|
|
178
|
+
stop_signals.each_with_index do |value, i|
|
|
179
|
+
expected, label = i.even? ? [Symbol, 'a symbol (signal)'] : [Integer, 'a number (delay)']
|
|
180
|
+
next if value.is_a?(expected)
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
$stderr.puts 'Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!'
|
|
107
|
-
exit(6)
|
|
182
|
+
config_error!("Config Error: Invalid stop_signals! Expected #{label} at position #{i} instead of '#{value}'.")
|
|
108
183
|
end
|
|
184
|
+
config_error!('Config Error: Invalid stop_signals! Expected an odd number of elements.') unless stop_signals.size.odd?
|
|
185
|
+
|
|
186
|
+
stop_grace_time = attributes[:stop_grace_time]
|
|
187
|
+
delays = stop_signals.select.with_index { |_, i| i.odd? }
|
|
188
|
+
return unless stop_grace_time.nil? || stop_grace_time <= delays.inject(0, :+)
|
|
189
|
+
|
|
190
|
+
config_error!('Config Error: Stop_grace_time should be greater than the sum of stop_signals delays!')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Warn when bluepill can't find the process's pid
|
|
194
|
+
#
|
|
195
|
+
# Bluepill only writes the default pid file for processes it daemonizes,
|
|
196
|
+
# so it cannot find the pid of any other process that uses it.
|
|
197
|
+
#
|
|
198
|
+
# @param [ProcessProxy] process the process's proxy
|
|
199
|
+
# @param [String, nil] default_pid_file the default pid file, or nil when
|
|
200
|
+
# the process sets its own
|
|
201
|
+
#
|
|
202
|
+
# @return [void]
|
|
203
|
+
#
|
|
204
|
+
# @api private
|
|
205
|
+
def warn_unless_pid_known(process, default_pid_file)
|
|
206
|
+
return if attributes[:daemonize] || attributes[:pid_command] || !attributes.fetch(:pid_file).eql?(default_pid_file)
|
|
207
|
+
|
|
208
|
+
warning = "Config Warning: '#{process.name}' sets no daemonize, pid_file or pid_command, " \
|
|
209
|
+
"so bluepill cannot find its pid unless start_command writes it to #{default_pid_file}. " \
|
|
210
|
+
'Set daemonize = true if start_command runs in the foreground.'
|
|
211
|
+
$stderr.puts warning
|
|
109
212
|
end
|
|
110
213
|
|
|
214
|
+
# Print a config error and exit with status 6
|
|
215
|
+
#
|
|
216
|
+
# @param [String] message the error
|
|
217
|
+
#
|
|
218
|
+
# @return [void] it never returns
|
|
219
|
+
#
|
|
220
|
+
# @api private
|
|
221
|
+
def config_error!(message)
|
|
222
|
+
$stderr.puts message
|
|
223
|
+
exit(6)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Exit unless a monitored child has a stop_command
|
|
227
|
+
#
|
|
228
|
+
# @param [ProcessProxy] child the child's proxy
|
|
229
|
+
#
|
|
230
|
+
# @return [void]
|
|
231
|
+
#
|
|
232
|
+
# @api private
|
|
111
233
|
def validate_child_process!(child)
|
|
112
234
|
return if child.attributes.key?(:stop_command)
|
|
235
|
+
|
|
113
236
|
$stderr.puts "Config Error: Invalid child process monitor for #{child.name}"
|
|
114
237
|
$stderr.puts 'You must specify a stop command to monitor child processes.'
|
|
115
238
|
exit(6)
|