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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +820 -0
  3. data/CONTRIBUTING.md +31 -0
  4. data/LICENSE +1 -1
  5. data/README.md +64 -29
  6. data/bin/bluepill +1 -1
  7. data/bluepill.gemspec +17 -6
  8. data/lib/bluepill/application.rb +309 -41
  9. data/lib/bluepill/condition_watch.rb +103 -8
  10. data/lib/bluepill/controller.rb +124 -13
  11. data/lib/bluepill/dsl/app_proxy.rb +102 -3
  12. data/lib/bluepill/dsl/process_factory.rb +183 -60
  13. data/lib/bluepill/dsl/process_proxy.rb +132 -14
  14. data/lib/bluepill/dsl.rb +27 -5
  15. data/lib/bluepill/group.rb +142 -9
  16. data/lib/bluepill/logger.rb +120 -5
  17. data/lib/bluepill/process.rb +571 -200
  18. data/lib/bluepill/process_commands.rb +262 -0
  19. data/lib/bluepill/process_conditions/always_true.rb +18 -4
  20. data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
  21. data/lib/bluepill/process_conditions/file_time.rb +40 -4
  22. data/lib/bluepill/process_conditions/http.rb +81 -23
  23. data/lib/bluepill/process_conditions/mem_usage.rb +61 -2
  24. data/lib/bluepill/process_conditions/process_condition.rb +43 -0
  25. data/lib/bluepill/process_conditions/running_time.rb +36 -1
  26. data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
  27. data/lib/bluepill/process_conditions.rb +15 -1
  28. data/lib/bluepill/process_journal.rb +256 -104
  29. data/lib/bluepill/process_statistics.rb +33 -3
  30. data/lib/bluepill/socket.rb +63 -16
  31. data/lib/bluepill/system/commands.rb +286 -0
  32. data/lib/bluepill/system.rb +192 -228
  33. data/lib/bluepill/trigger.rb +110 -14
  34. data/lib/bluepill/triggers/flapping.rb +84 -3
  35. data/lib/bluepill/util/rotational_array.rb +24 -1
  36. data/lib/bluepill/version.rb +71 -19
  37. data/lib/bluepill.rb +25 -2
  38. data/sig/bluepill.rbs +926 -0
  39. data/sig/manifest.yaml +7 -0
  40. metadata +16 -34
  41. data/bin/sample_forking_server +0 -66
  42. data/lib/bluepill/application/client.rb +0 -6
  43. data/lib/bluepill/application/server.rb +0 -21
@@ -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
- def initialize(options = {})
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[:base_dir]
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.strip.empty?
98
+ log_file_location = log_file if log_file_location.to_s.rstrip.empty?
37
99
 
38
100
  requested_pattern = args.first
39
- grep_pattern = self.grep_pattern(application, requested_pattern)
101
+ pattern = grep_pattern(application, requested_pattern)
40
102
 
41
- tail = "tail -n 100 -f #{log_file_location} | grep -E '#{grep_pattern}'"
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
- $stderr.puts response.backtrace.join("\n")
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), '.*'].compact.join
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) && File.read(pid_file).to_i
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
- if version != Bluepill::Version
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
- attr_accessor(*APP_ATTRIBUTES)
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] = send(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
- @@process_keys = {}
6
- @@pid_files = {}
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
- [:start_grace_time, :stop_grace_time, :restart_grace_time].each { |a| attributes[a] = @attributes[a] }
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 + '.pid')
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
- # validate uniqueness of group:process
49
- process_key = [process.attributes[:group], process.name].join(':')
50
- if @@process_keys.key?(process_key)
51
- $stderr.print "Config Error: You have two entries for the process name '#{process.name}'"
52
- $stderr.print " in the group '#{process.attributes[:group]}'" if process.attributes.key?(:group)
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
- # validate required attributes
60
- [:start_command].each do |required_attr|
61
- unless process.attributes.key?(required_attr)
62
- $stderr.puts "Config Error: You must specify a #{required_attr} for '#{process.name}'"
63
- exit(6)
64
- end
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
- # validate uniqueness of pid files
68
- pid_key = process.attributes[:pid_file].strip
69
- if @@pid_files.key?(pid_key)
70
- $stderr.puts "Config Error: You have two entries with the pid file: #{pid_key}"
71
- exit(6)
72
- else
73
- @@pid_files[pid_key] = 0
74
- end
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
- # validate stop_signals array
77
- stop_grace_time = process.attributes[:stop_grace_time]
78
- stop_signals = process.attributes[:stop_signals]
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
- unless stop_signals.size.odd?
101
- $stderr.puts 'Config Error: Invalid stop_signals! Expected an odd number of elements.'
102
- exit(6)
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
- if stop_grace_time.nil? || stop_grace_time <= delay_sum
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)