bluepill 0.3.0 → 0.3.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -1
  3. data/README.md +4 -6
  4. data/bin/bluepill +13 -6
  5. data/bluepill.gemspec +7 -2
  6. data/lib/bluepill/application/lifecycle.rb +8 -5
  7. data/lib/bluepill/application/listener.rb +1 -3
  8. data/lib/bluepill/application.rb +5 -3
  9. data/lib/bluepill/condition_watch.rb +3 -2
  10. data/lib/bluepill/controller.rb +5 -2
  11. data/lib/bluepill/daemonize.rb +52 -0
  12. data/lib/bluepill/dsl/attributes.rb +2 -2
  13. data/lib/bluepill/dsl/process_factory/child_factory.rb +5 -2
  14. data/lib/bluepill/dsl/process_factory/registry.rb +0 -2
  15. data/lib/bluepill/dsl/process_factory.rb +3 -1
  16. data/lib/bluepill/dsl/process_proxy.rb +28 -1
  17. data/lib/bluepill/dsl.rb +2 -2
  18. data/lib/bluepill/logger.rb +10 -2
  19. data/lib/bluepill/process/children.rb +2 -1
  20. data/lib/bluepill/process/pid.rb +2 -0
  21. data/lib/bluepill/process/stop_signals.rb +5 -2
  22. data/lib/bluepill/process/triggers.rb +2 -2
  23. data/lib/bluepill/process/watches.rb +2 -2
  24. data/lib/bluepill/process.rb +2 -2
  25. data/lib/bluepill/process_commands/execution.rb +4 -4
  26. data/lib/bluepill/process_commands.rb +1 -1
  27. data/lib/bluepill/process_conditions/file_time.rb +1 -1
  28. data/lib/bluepill/process_conditions/http.rb +2 -1
  29. data/lib/bluepill/socket.rb +1 -1
  30. data/lib/bluepill/system/command_options.rb +5 -4
  31. data/lib/bluepill/system/command_result.rb +1 -1
  32. data/lib/bluepill/system/command_spawn.rb +244 -0
  33. data/lib/bluepill/system/commands.rb +6 -22
  34. data/lib/bluepill/system/daemon.rb +79 -2
  35. data/lib/bluepill/system/usage.rb +20 -0
  36. data/lib/bluepill/version.rb +11 -11
  37. data/sig/bluepill/dsl.rbs +4 -0
  38. data/sig/bluepill/system.rbs +48 -13
  39. data/sig/bluepill.rbs +1 -0
  40. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa9f8a199b7c2f30858345bc94b934e3a15be6776595ea4481c61722340812b8
4
- data.tar.gz: b33c36a84b7bbe956866889eca7838211f4d40942e75b3e6fefd33b6c81b6958
3
+ metadata.gz: '0208aabbcc97f59a0e1186f9d01d58b96a3193b378a33341e38914f5817e2dde'
4
+ data.tar.gz: a985b9962e81fcce8a800e3154a971fcf8864cf43905f31abc91fed6cfdf0e2d
5
5
  SHA512:
6
- metadata.gz: 42b736d7e86289628a9a0b057f01d9e0b379a42048b6903da13090429a4831255adcefd07db3cccc3ab19b816ad9281ad58d692779e171a3ea767cb24c0f96f6
7
- data.tar.gz: e8dcc83be7c7a2df1f6f030f13dc11ef75bd98b087dce958cbce07f13812193693bdd7dfafd3ee2e8d9753b913b41341bb8b1db5d588882aa748f789d7ff9445
6
+ metadata.gz: e260492f6536962844cf956db8493aa730a28c53a08a18dbca4a1ac53d53e54f9c1b9e2e6edfad165205a0b4021844878099ccb25a486b8fa7a9fdd2d0125ef8
7
+ data.tar.gz: 848b4037e5cf18f6b4ad5db27fd5dbe22e443101619af3c6e4a8e944bff85189ebafcb6353615bb7df73c40a65dda1c6294ab5ca0849f23e5c19d2a3237f2126
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.2] - 2026-11-02
11
+
12
+ ### Removed
13
+
14
+ - **Breaking:** support for Ruby 3.0 and 3.1, and for JRuby 9.4, which runs Ruby 3.1 code. Bluepill 0.3.2 needs Ruby 3.2 or newer, or JRuby 10.0 or newer, which needs Java 21. Bluepill 0.3.0 still runs on them.
15
+
16
+ ### Fixed
17
+
18
+ - Bluepill runs on JRuby, in the foreground and as a daemon. It forked to start every process and command, which the JVM can't do, so on JRuby it started none. On JRuby it now spawns them, and starts a new bluepill to be the daemon. A process that bluepill starts there runs in a process group of its own, as a forked one's new session does, and stays bluepill's child, which collects its exit status. Spawned commands can't switch to another user, so on JRuby a config that sets `uid`, `gid` or `supplementary_groups` fails to load when bluepill runs as root. CRuby runs bluepill as before.
19
+
10
20
  ## [0.3.0] - 2026-11-02
11
21
 
12
22
  ### Upgrading from 0.2
@@ -992,7 +1002,8 @@ Initial release.
992
1002
  - Added the `bluepill` CLI with the `load`, `status`, `start`, `stop`, `restart`, `unmonitor`, `quit` and `log` commands.
993
1003
  - Added syslog logging.
994
1004
 
995
- [unreleased]: https://github.com/bluepill-rb/bluepill/compare/v0.3.0...HEAD
1005
+ [unreleased]: https://github.com/bluepill-rb/bluepill/compare/v0.3.2...HEAD
1006
+ [0.3.2]: https://github.com/bluepill-rb/bluepill/compare/v0.3.0...v0.3.2
996
1007
  [0.3.0]: https://github.com/bluepill-rb/bluepill/compare/v0.2.9...v0.3.0
997
1008
  [0.2.9]: https://github.com/bluepill-rb/bluepill/compare/v0.2.7...v0.2.9
998
1009
  [0.2.7]: https://github.com/bluepill-rb/bluepill/compare/v0.2.5...v0.2.7
data/README.md CHANGED
@@ -198,6 +198,8 @@ end
198
198
 
199
199
  These only apply when bluepill runs as root. Every command a process runs, such as its `start_command`, `stop_command` and `pre_start_command`, runs as its user and groups, and so do the commands of its children with `monitor_children`.
200
200
 
201
+ On JRuby, bluepill can't run commands as another user or group, since the JVM can't fork, so a config that sets `uid`, `gid` or `supplementary_groups` fails to load when bluepill runs as root. Run it on CRuby to use them.
202
+
201
203
  To track resources of child processes, use `:include_children`:
202
204
  ```ruby
203
205
  Bluepill.application("app_name") do |app|
@@ -497,9 +499,6 @@ end
497
499
 
498
500
  Note that You must define only one application per config when using foreground mode.
499
501
 
500
-
501
- JRuby allows you to run bluepill only in the foreground.
502
-
503
502
  ## Links
504
503
 
505
504
  * Code: [http://github.com/bluepill-rb/bluepill](http://github.com/bluepill-rb/bluepill)
@@ -512,10 +511,9 @@ JRuby allows you to run bluepill only in the foreground.
512
511
  This library aims to support and is [tested against][ci] the following Ruby
513
512
  implementations:
514
513
 
515
- * Ruby 3.0 through 3.4
514
+ * Ruby 3.2 through 3.4
516
515
  * Ruby 4.0
517
- * JRuby 9.4 (only in the foreground)
518
- * JRuby 10.0 and 10.1 (only in the foreground)
516
+ * JRuby 10.0 and 10.1, without `uid`, `gid` and `supplementary_groups`
519
517
 
520
518
  If something doesn't work on one of these interpreters, please open an issue.
521
519
 
data/bin/bluepill CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  require "optparse"
4
5
  require "rbconfig"
5
6
  require "bluepill"
@@ -7,7 +8,8 @@ require "bluepill"
7
8
  # Default options
8
9
  options = {
9
10
  log_file: "/var/log/bluepill.log",
10
- base_dir: ENV["BLUEPILL_BASE_DIR"] || (::Process.euid.zero? ? "/var/run/bluepill" : File.join(ENV.fetch("HOME", nil), ".bluepill")),
11
+ base_dir: ENV["BLUEPILL_BASE_DIR"] ||
12
+ (::Process.euid.zero? ? "/var/run/bluepill" : File.join(ENV.fetch("HOME", nil), ".bluepill")),
11
13
  privileged: true,
12
14
  timeout: 10,
13
15
  attempts: 1
@@ -19,7 +21,8 @@ OptionParser.new do |opts|
19
21
  options[:log_file] = file
20
22
  end
21
23
 
22
- opts.on("-c", "--base-dir DIR", "Directory to store bluepill socket and pid files, defaults to #{options[:base_dir]}") do |base_dir|
24
+ opts.on("-c", "--base-dir DIR",
25
+ "Directory to store bluepill socket and pid files, defaults to #{options[:base_dir]}") do |base_dir|
23
26
  options[:base_dir] = base_dir
24
27
  end
25
28
 
@@ -28,15 +31,18 @@ OptionParser.new do |opts|
28
31
  exit
29
32
  end
30
33
 
31
- opts.on("--[no-]privileged", "Allow/disallow to run #{$PROGRAM_NAME} as non-privileged process. disallowed by default") do |v|
34
+ opts.on("--[no-]privileged",
35
+ "Allow/disallow to run #{$PROGRAM_NAME} as non-privileged process. disallowed by default") do |v|
32
36
  options[:privileged] = v
33
37
  end
34
38
 
35
- opts.on("-t", "--timeout Seconds", Integer, "Timeout for commands sent to the daemon, in seconds. Defaults to 10.") do |timeout|
39
+ opts.on("-t", "--timeout Seconds", Integer,
40
+ "Timeout for commands sent to the daemon, in seconds. Defaults to 10.") do |timeout|
36
41
  options[:timeout] = timeout
37
42
  end
38
43
 
39
- opts.on("--attempts Count", Integer, "Attempts for commands sent to the daemon, in seconds. Defaults to 1.") do |attempts|
44
+ opts.on("--attempts Count", Integer,
45
+ "Attempts for commands sent to the daemon, in seconds. Defaults to 1.") do |attempts|
40
46
  options[:attempts] = attempts
41
47
  end
42
48
 
@@ -91,7 +97,8 @@ elsif APPLICATION_COMMANDS.include?(ARGV.first)
91
97
  exit(1)
92
98
  else
93
99
  # There are none running AND they aren't trying to start one
94
- $stderr.puts "Error: There are no running bluepill daemons.\nTo start a bluepill daemon, use: bluepill load <config file>"
100
+ $stderr.puts "Error: There are no running bluepill daemons.\n" \
101
+ "To start a bluepill daemon, use: bluepill load <config file>"
95
102
  exit(2)
96
103
  end
97
104
  end
data/bluepill.gemspec CHANGED
@@ -12,7 +12,12 @@ Gem::Specification.new do |spec|
12
12
  spec.email = ["entombedvirus@gmail.com"]
13
13
  spec.homepage = "https://github.com/bluepill-rb/bluepill"
14
14
  spec.summary = "A process monitor written in Ruby with stability and minimalism in mind."
15
- spec.description = "Bluepill keeps your daemons up while taking up as little resources as possible. After all you probably want the resources of your server to be used by whatever daemons you are running rather than the thing that's supposed to make sure they are brought back up, should they die or misbehave."
15
+ spec.description = <<~DESCRIPTION.tr("\n", " ").strip
16
+ Bluepill keeps your daemons up while taking up as little resources as possible.
17
+ After all you probably want the resources of your server to be used by whatever
18
+ daemons you are running rather than the thing that's supposed to make sure they
19
+ are brought back up, should they die or misbehave.
20
+ DESCRIPTION
16
21
  spec.license = "MIT"
17
22
 
18
23
  spec.metadata = {
@@ -22,7 +27,7 @@ Gem::Specification.new do |spec|
22
27
  "rubygems_mfa_required" => "true"
23
28
  }
24
29
 
25
- spec.required_ruby_version = ">= 3.0"
30
+ spec.required_ruby_version = ">= 3.2"
26
31
 
27
32
  # sig has the RBS signatures, and the standard libraries they use in manifest.yaml
28
33
  spec.executables = %w[bluepill bpsv]
@@ -13,8 +13,9 @@ module Bluepill
13
13
  # Start the daemon
14
14
  #
15
15
  # It creates the pids directory, stops a previous daemon for the
16
- # application, clears journal locks it left behind, daemonizes unless
17
- # it runs in the foreground, traps signals, opens the socket, and runs
16
+ # application, clears journal locks it left behind, daemonizes, or in
17
+ # the foreground leads a process group of its own, traps signals,
18
+ # opens the socket, and runs
18
19
  # until a signal stops it. Processes that are already running, from
19
20
  # their pid files, are monitored again, not restarted, so bluepill can
20
21
  # be upgraded without restarting them.
@@ -30,9 +31,10 @@ module Bluepill
30
31
  setup_pids_dir
31
32
  kill_previous_bluepill
32
33
  ProcessJournal.clear_all_atomic_fs_locks
33
- become_process_group_leader
34
+ # Daemonizing starts a new session, which has a process group of its
35
+ # own, and a process that leads a process group already can't
36
+ foreground? ? become_process_group_leader : Daemonize.daemonize
34
37
  # Trapped after daemonizing, which ignores HUP
35
- Daemonize.daemonize unless foreground?
36
38
  setup_signal_traps
37
39
  logger.reopen
38
40
 
@@ -130,7 +132,8 @@ module Bluepill
130
132
  #
131
133
  # @api private
132
134
  def setup_logger
133
- self.logger = ProcessJournal.logger = Logger.new(log_file: log_file, log_level: @log_level, stdout: foreground?).prefix_with(name)
135
+ logger = Logger.new(log_file:, log_level: @log_level, stdout: foreground?).prefix_with(name)
136
+ self.logger = ProcessJournal.logger = logger
134
137
  rescue SystemCallError => e
135
138
  raise unwritable(e)
136
139
  end
@@ -91,10 +91,8 @@ module Bluepill
91
91
  #
92
92
  # @api private
93
93
  def daemon_command(command)
94
- # The backtrace is caller's lines, with no locations, so Ruby 3.1's
95
- # error_highlight doesn't add a snippet of this file to the message
96
94
  DAEMON_COMMANDS.detect { |known| known.name.eql?(command) } ||
97
- raise(NoMethodError, "Unknown command `#{command}`", caller)
95
+ raise(NoMethodError, "Unknown command `#{command}`")
98
96
  end
99
97
 
100
98
  # Log an error that answering a client raised, with its backtrace
@@ -148,7 +148,7 @@ module Bluepill
148
148
  # @return [Object] what the block returns
149
149
  #
150
150
  # @api private
151
- def synchronize(&block) = @mutex.synchronize(&block)
151
+ def synchronize(&) = @mutex.synchronize(&)
152
152
 
153
153
  # Start the daemon and monitor the application's processes
154
154
  #
@@ -168,7 +168,8 @@ module Bluepill
168
168
  log_crash(e)
169
169
  raise if e.is_a?(Error)
170
170
 
171
- raise StartupError, ["Failed to start bluepill:", format("%s `%s`", e.class.name, e.message), e.backtrace].join("\n")
171
+ raise StartupError,
172
+ ["Failed to start bluepill:", format("%s `%s`", e.class.name, e.message), e.backtrace].join("\n")
172
173
  end
173
174
 
174
175
  # @!method start(group_name = nil, process_name = nil)
@@ -271,7 +272,8 @@ module Bluepill
271
272
  #
272
273
  # @api private
273
274
  def log_crash(error)
274
- [format("Bluepill stopped: %s `%s`", error.class, error.message), *error.backtrace].each { |line| logger.crit(line) }
275
+ lines = [format("Bluepill stopped: %s `%s`", error.class, error.message), *error.backtrace]
276
+ lines.each { |line| logger.crit(line) }
275
277
  end
276
278
 
277
279
  # Send a command to a process, a group, or every process
@@ -75,7 +75,8 @@ module Bluepill
75
75
  # @raise [ConfigError] for an event a check can't fire
76
76
  #
77
77
  # @api private
78
- def initialize(name, logger: nil, fires: :restart, every: nil, times: 1, include_children: false, **condition_options)
78
+ def initialize(name, logger: nil, fires: :restart, every: nil, times: 1, include_children: false,
79
+ **condition_options)
79
80
  @name = name
80
81
  @logger = logger
81
82
  @fires = events_to_fire(Array(fires))
@@ -105,7 +106,7 @@ module Bluepill
105
106
  return events if unknown.empty?
106
107
 
107
108
  raise ConfigError, "#{@name} can't fire #{unknown.collect(&:inspect).join(", ")}: " \
108
- "a check fires #{known.collect(&:inspect).join(", ")}"
109
+ "a check fires #{known.collect(&:inspect).join(", ")}"
109
110
  end
110
111
 
111
112
  # Create the check, with the options the watch doesn't take for itself
@@ -193,7 +193,8 @@ module Bluepill
193
193
  # @api private
194
194
  def send_process_command(application, command, *args)
195
195
  unless Application::PROCESS_COMMANDS.include?(command.to_sym)
196
- raise UnknownCommandError, format("Unknown command `%s` (or application `%s` has not been loaded yet)", command, command)
196
+ raise UnknownCommandError,
197
+ format("Unknown command `%s` (or application `%s` has not been loaded yet)", command, command)
197
198
  end
198
199
 
199
200
  # these need to be sent to the daemon and the results printed out
@@ -235,7 +236,9 @@ module Bluepill
235
236
  version = Socket.client_command(base_dir, application, "version")
236
237
  return if Version.to_s.eql?(version)
237
238
 
238
- raise VersionMismatchError, version.instance_of?(String) ? "#{OUT_OF_DATE}\nDaemon Version: #{version}, CLI Version: #{Version}" : OUT_OF_DATE
239
+ raise VersionMismatchError, OUT_OF_DATE unless version.instance_of?(String)
240
+
241
+ raise VersionMismatchError, "#{OUT_OF_DATE}\nDaemon Version: #{version}, CLI Version: #{Version}"
239
242
  rescue ArgumentError
240
243
  raise VersionMismatchError, OUT_OF_DATE
241
244
  end
@@ -27,6 +27,8 @@
27
27
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28
28
  # OTHER DEALINGS IN THE SOFTWARE.
29
29
 
30
+ require "rbconfig"
31
+
30
32
  module Bluepill
31
33
  # Turn the bluepill daemon, or a block, into a daemon process
32
34
  #
@@ -35,6 +37,9 @@ module Bluepill
35
37
  # it started in, closes the files it inherited, and points its standard
36
38
  # streams at /dev/null.
37
39
  #
40
+ # A Ruby that can't fork, such as JRuby, starts a new bluepill to be the
41
+ # daemon instead, which starts the new session itself.
42
+ #
38
43
  # @api private
39
44
  module Daemonize
40
45
  # Raised when a daemon can't detach from its controlling terminal
@@ -45,6 +50,20 @@ module Bluepill
45
50
  # The highest file descriptor that close_io closes
46
51
  MAX_FD = 8191
47
52
 
53
+ # Set in the environment of the bluepill that a bluepill that can't
54
+ # fork starts to be its daemon
55
+ DAEMON_ENV = "BLUEPILL_DAEMON"
56
+
57
+ # Return whether this Ruby can fork
58
+ #
59
+ # JRuby can't, since the JVM runs threads of its own that a fork would
60
+ # leave behind. Bluepill spawns its commands there instead.
61
+ #
62
+ # @return [Boolean]
63
+ #
64
+ # @api private
65
+ def self.can_fork? = ::Process.respond_to?(:fork)
66
+
48
67
  # Fork, retrying every 5 seconds while the system is out of processes
49
68
  #
50
69
  # @return [Integer, nil] the child's pid in the parent, nil in the child
@@ -132,6 +151,8 @@ module Bluepill
132
151
  #
133
152
  # @api private
134
153
  def self.daemonize
154
+ return daemonize_without_fork unless can_fork?
155
+
135
156
  exit if safefork
136
157
 
137
158
  session = detach
@@ -141,6 +162,37 @@ module Bluepill
141
162
  session
142
163
  end
143
164
 
165
+ # Turn the calling process into a daemon, in a Ruby that can't fork
166
+ #
167
+ # The first time, it starts a new bluepill that runs the same pill file
168
+ # with DAEMON_ENV set, and exits, so `bluepill load` returns. In that
169
+ # bluepill, which is new, so it inherited no files to close and its
170
+ # standard streams are /dev/null already, it starts a new session and
171
+ # leaves the directory it started in.
172
+ #
173
+ # @return [Integer] the daemon's session id
174
+ #
175
+ # @api private
176
+ def self.daemonize_without_fork
177
+ start_daemon_bluepill unless ENV.delete(DAEMON_ENV)
178
+
179
+ session = detach
180
+ Dir.chdir("/")
181
+ session
182
+ end
183
+
184
+ # Start a bluepill that runs the same pill file to be the daemon, and exit
185
+ #
186
+ # @return [void] it never returns, since it exits
187
+ #
188
+ # @api private
189
+ def self.start_daemon_bluepill
190
+ dir = __dir__ #: String
191
+ args = ["-I#{File.expand_path("..", dir)}", "-rbluepill", File.expand_path($PROGRAM_NAME), *ARGV]
192
+ ::Process.spawn({DAEMON_ENV => "1"}, RbConfig.ruby, *args, in: File::NULL, out: File::NULL, err: File::NULL)
193
+ exit
194
+ end
195
+
144
196
  # Start a new session, which leaves the controlling terminal
145
197
  #
146
198
  # @return [Integer] the new session's id
@@ -89,8 +89,8 @@ module Bluepill
89
89
  # @raise [ConfigError] for a method the proxy doesn't have
90
90
  #
91
91
  # @api private
92
- def evaluate(&block)
93
- instance_eval(&block)
92
+ def evaluate(&)
93
+ instance_eval(&)
94
94
  rescue NameError => e
95
95
  raise unless e.receiver.equal?(self)
96
96
 
@@ -49,7 +49,7 @@ module Bluepill
49
49
  # @api private
50
50
  def create_child_process(name, pid, logger)
51
51
  child = ProcessProxy.new(name, @attributes, @child_process_block, actual_pid: pid)
52
- process = child.to_process(logger: logger)
52
+ process = child.to_process(logger:)
53
53
  process.determine_initial_state
54
54
  process
55
55
  end
@@ -71,8 +71,11 @@ module Bluepill
71
71
  # @api private
72
72
  def validate!(parent_name)
73
73
  child = ProcessProxy.new("<child of #{parent_name}>", @attributes, @child_process_block)
74
- raise ConfigError, "Config Error: You must specify a stop_command for the children of '#{parent_name}'" unless child.stop_command
74
+ unless child.stop_command
75
+ raise ConfigError, "Config Error: You must specify a stop_command for the children of '#{parent_name}'"
76
+ end
75
77
 
78
+ child.validate_privileges!
76
79
  child.to_process
77
80
  end
78
81
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Bluepill
6
4
  class ProcessFactory
7
5
  # The process names and pid files that an application's config has used
@@ -109,6 +109,7 @@ module Bluepill
109
109
  validate_unique_pid_file!(process)
110
110
  validate_stop_signals!(process)
111
111
  validate_on_start_timeout!(process)
112
+ process.validate_privileges!
112
113
  end
113
114
 
114
115
  # Fail if another process in the group has the same name
@@ -208,7 +209,8 @@ module Bluepill
208
209
  event = process.on_start_timeout
209
210
  return if events.include?(event)
210
211
 
211
- config_error!("Config Error: on_start_timeout for '#{process.name}' must be one of #{events.collect(&:inspect).join(", ")}, not #{event.inspect}")
212
+ config_error!("Config Error: on_start_timeout for '#{process.name}' must be one of " \
213
+ "#{events.collect(&:inspect).join(", ")}, not #{event.inspect}")
212
214
  end
213
215
 
214
216
  # Warn when bluepill can't find the process's pid
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bluepill/dsl/attributes"
4
4
  require "bluepill/process/attributes"
5
+ require "bluepill/daemonize"
5
6
 
6
7
  module Bluepill
7
8
  # The self of a process block in a config file
@@ -25,6 +26,9 @@ module Bluepill
25
26
  # monitor_children sets the two attributes it leaves out
26
27
  attribute(*(Process::Attributes::CONFIGURABLE_ATTRIBUTES - %i[monitor_children child_process_factory]), :group)
27
28
 
29
+ # The attributes that switch the user or groups that commands run as
30
+ PRIVILEGE_ATTRIBUTES = %i[uid gid supplementary_groups].freeze
31
+
28
32
  # Return the process's name
29
33
  #
30
34
  # @return [String] the name
@@ -119,7 +123,9 @@ module Bluepill
119
123
  #
120
124
  # @api public
121
125
  def monitor_children(&child_process_block)
122
- raise ConfigError, "monitor_children for process '#{@name}' needs a block that configures the children" unless child_process_block
126
+ unless child_process_block
127
+ raise ConfigError, "monitor_children for process '#{@name}' needs a block that configures the children"
128
+ end
123
129
 
124
130
  @child_process_block = child_process_block
125
131
  end
@@ -139,6 +145,27 @@ module Bluepill
139
145
  Process.new(@name, @checks, actual_pid: @actual_pid, **attributes.except(:group), **options)
140
146
  end
141
147
 
148
+ # Fail if the process sets a user or groups that bluepill can't switch to
149
+ #
150
+ # A Ruby that can't fork, such as JRuby, spawns commands, which can't
151
+ # switch users or groups. Bluepill only switches when it runs as root,
152
+ # so they fail the config only then.
153
+ #
154
+ # @return [void]
155
+ #
156
+ # @raise [ConfigError] if it sets uid, gid or supplementary_groups
157
+ #
158
+ # @api private
159
+ def validate_privileges!
160
+ return if Daemonize.can_fork? || !::Process::Sys.geteuid.zero?
161
+
162
+ set = PRIVILEGE_ATTRIBUTES.select { |name| attributes[name] }
163
+ return if set.empty?
164
+
165
+ raise ConfigError, "Config Error: #{attribute_owner} sets #{set.join(", ")}, but bluepill " \
166
+ "can't run commands as another user or group on #{RUBY_ENGINE}, which can't fork"
167
+ end
168
+
142
169
  # Name the process, for an unknown attribute's error
143
170
  #
144
171
  # @return [String] the process's name
data/lib/bluepill/dsl.rb CHANGED
@@ -30,11 +30,11 @@ module Bluepill
30
30
  # @return [void]
31
31
  #
32
32
  # @api public
33
- def self.application(app_name, **options, &block)
33
+ def self.application(app_name, **options, &)
34
34
  app_proxy = AppProxy.new(app_name, **options)
35
35
  # evaluate also passes the proxy to a block that takes an argument, and
36
36
  # turns a misspelled attribute into a ConfigError
37
- app_proxy.evaluate(&block)
37
+ app_proxy.evaluate(&)
38
38
  app_proxy.app.load
39
39
  rescue Error => e
40
40
  location = e.location if e.is_a?(ConfigError)
@@ -218,8 +218,16 @@ module Bluepill
218
218
  # The ::Logger method for each syslog priority
219
219
  #
220
220
  # @api private
221
- LOGGER_EQUIVALENTS =
222
- {debug: :debug, err: :error, warning: :warn, info: :info, emerg: :fatal, alert: :warn, crit: :fatal, notice: :info}.freeze
221
+ LOGGER_EQUIVALENTS = {
222
+ debug: :debug,
223
+ err: :error,
224
+ warning: :warn,
225
+ info: :info,
226
+ emerg: :fatal,
227
+ alert: :warn,
228
+ crit: :fatal,
229
+ notice: :info
230
+ }.freeze
223
231
 
224
232
  LOG_METHODS.each do |method|
225
233
  next if method == LOGGER_EQUIVALENTS[method]
@@ -29,7 +29,8 @@ module Bluepill
29
29
  new_children_pids = System.get_children(actual_pid) - @children.collect(&:actual_pid)
30
30
  return if new_children_pids.empty?
31
31
 
32
- logger.info "Existing children: #{@children.collect(&:actual_pid).join(",")}. Got new children: #{new_children_pids} for #{actual_pid}"
32
+ logger.info "Existing children: #{@children.collect(&:actual_pid).join(",")}. " \
33
+ "Got new children: #{new_children_pids} for #{actual_pid}"
33
34
  add_children(new_children_pids)
34
35
  end
35
36
 
@@ -47,6 +47,8 @@ module Bluepill
47
47
  # could include bluepill itself, so only signal a real pid
48
48
  raise "invalid pid #{pid}" unless pid > 0
49
49
 
50
+ # A spawned daemon that exited is a zombie until it's collected
51
+ System.reap(pid)
50
52
  ::Process.kill(code, pid)
51
53
  true
52
54
  rescue => e
@@ -111,9 +111,12 @@ module Bluepill
111
111
  expected, label = i.even? ? [Symbol, "a symbol (signal)"] : [Integer, "a number (delay)"]
112
112
  next if value.is_a?(expected)
113
113
 
114
- raise(ConfigError, "Config Error: Invalid stop_signals! Expected #{label} at position #{i} instead of '#{value}'.")
114
+ raise(ConfigError,
115
+ "Config Error: Invalid stop_signals! Expected #{label} at position #{i} instead of '#{value}'.")
115
116
  end
116
- raise(ConfigError, "Config Error: Invalid stop_signals! Expected an odd number of elements.") unless signals.size.odd?
117
+ return if signals.size.odd?
118
+
119
+ raise(ConfigError, "Config Error: Invalid stop_signals! Expected an odd number of elements.")
117
120
  end
118
121
  end
119
122
  end
@@ -37,7 +37,7 @@ module Bluepill
37
37
  # @return [Triggers] the triggers
38
38
  #
39
39
  # @api private
40
- def add(klass, process, **options) = self << klass.new(process, **options, logger: logger)
40
+ def add(klass, process, **options) = self << klass.new(process, **options, logger:)
41
41
 
42
42
  # Add a trigger
43
43
  #
@@ -58,7 +58,7 @@ module Bluepill
58
58
  # @return [Array<Trigger>] the triggers
59
59
  #
60
60
  # @api private
61
- def each(&block) = @triggers.each(&block)
61
+ def each(&) = @triggers.each(&)
62
62
 
63
63
  # Tell every trigger about a transition, before it happens
64
64
  #
@@ -46,7 +46,7 @@ module Bluepill
46
46
  # @return [Watches] the watches
47
47
  #
48
48
  # @api private
49
- def add(name, **options) = self << ConditionWatch.new(name, **options, logger: logger)
49
+ def add(name, **options) = self << ConditionWatch.new(name, **options, logger:)
50
50
 
51
51
  # Add a watch
52
52
  #
@@ -67,7 +67,7 @@ module Bluepill
67
67
  # @return [Array<ConditionWatch>] the watches
68
68
  #
69
69
  # @api private
70
- def each(&block) = @watches.each(&block)
70
+ def each(&) = @watches.each(&)
71
71
 
72
72
  # Forget what the watches have read, as when the process changes state
73
73
  #
@@ -228,8 +228,8 @@ module Bluepill
228
228
  # @api private
229
229
  def add_checks(checks, logger)
230
230
  @logger = logger
231
- @watches = Watches.new(logger: logger)
232
- @triggers = Triggers.new(logger: logger)
231
+ @watches = Watches.new(logger:)
232
+ @triggers = Triggers.new(logger:)
233
233
  checks.each { |name, options| add_check(name, options) }
234
234
  end
235
235
 
@@ -34,8 +34,8 @@ module Bluepill
34
34
  # @api private
35
35
  def command_options
36
36
  System::CommandOptions.new(
37
- uid: uid, gid: gid, supplementary_groups: supplementary_groups, working_dir: working_dir,
38
- environment: environment, stdin: stdin, stdout: stdout, stderr: stderr
37
+ uid:, gid:, supplementary_groups:, working_dir:,
38
+ environment:, stdin:, stdout:, stderr:
39
39
  )
40
40
  end
41
41
 
@@ -51,8 +51,8 @@ module Bluepill
51
51
  # transition happened
52
52
  #
53
53
  # @api private
54
- def with_timeout(secs, next_state, &blk)
55
- Timeout.timeout(secs, &blk)
54
+ def with_timeout(secs, next_state, &)
55
+ Timeout.timeout(secs, &)
56
56
  rescue Timeout::Error
57
57
  logger.err "Execution is taking longer than expected."
58
58
  logger.err "Did you forget to tell bluepill to daemonize this process?"
@@ -70,7 +70,7 @@ module Bluepill
70
70
  def daemonize_process(command)
71
71
  # ProcessFactory gives every process that bluepill daemonizes a pid file
72
72
  file = pid_file #: String
73
- daemon_id = System.daemonize(command, command_options, pid_file: file, logger: logger)
73
+ daemon_id = System.daemonize(command, command_options, pid_file: file, logger:)
74
74
  return unless daemon_id
75
75
 
76
76
  journal.append(daemon_id)
@@ -24,7 +24,7 @@ module Bluepill
24
24
  #
25
25
  # @api private
26
26
  def initialize(filename:, below:, **options)
27
- super(below: below, **options)
27
+ super(below:, **options)
28
28
  @filename = filename
29
29
  end
30
30
 
@@ -44,7 +44,8 @@ module Bluepill
44
44
  # @raise [NameError] for a kind that names no Net::HTTP response
45
45
  #
46
46
  # @api private
47
- def initialize(url:, kind: nil, pattern: nil, timeout: 5, open_timeout: timeout, read_timeout: timeout, verify_ssl: true, **options)
47
+ def initialize(url:, kind: nil, pattern: nil, timeout: 5, open_timeout: timeout, read_timeout: timeout,
48
+ verify_ssl: true, **options)
48
49
  super(**options)
49
50
  @verify_ssl = verify_ssl
50
51
  @uri = URI.parse(url) #: URI::HTTP