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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +4 -6
- data/bin/bluepill +13 -6
- data/bluepill.gemspec +7 -2
- data/lib/bluepill/application/lifecycle.rb +8 -5
- data/lib/bluepill/application/listener.rb +1 -3
- data/lib/bluepill/application.rb +5 -3
- data/lib/bluepill/condition_watch.rb +3 -2
- data/lib/bluepill/controller.rb +5 -2
- data/lib/bluepill/daemonize.rb +52 -0
- data/lib/bluepill/dsl/attributes.rb +2 -2
- data/lib/bluepill/dsl/process_factory/child_factory.rb +5 -2
- data/lib/bluepill/dsl/process_factory/registry.rb +0 -2
- data/lib/bluepill/dsl/process_factory.rb +3 -1
- data/lib/bluepill/dsl/process_proxy.rb +28 -1
- data/lib/bluepill/dsl.rb +2 -2
- data/lib/bluepill/logger.rb +10 -2
- data/lib/bluepill/process/children.rb +2 -1
- data/lib/bluepill/process/pid.rb +2 -0
- data/lib/bluepill/process/stop_signals.rb +5 -2
- data/lib/bluepill/process/triggers.rb +2 -2
- data/lib/bluepill/process/watches.rb +2 -2
- data/lib/bluepill/process.rb +2 -2
- data/lib/bluepill/process_commands/execution.rb +4 -4
- data/lib/bluepill/process_commands.rb +1 -1
- data/lib/bluepill/process_conditions/file_time.rb +1 -1
- data/lib/bluepill/process_conditions/http.rb +2 -1
- data/lib/bluepill/socket.rb +1 -1
- data/lib/bluepill/system/command_options.rb +5 -4
- data/lib/bluepill/system/command_result.rb +1 -1
- data/lib/bluepill/system/command_spawn.rb +244 -0
- data/lib/bluepill/system/commands.rb +6 -22
- data/lib/bluepill/system/daemon.rb +79 -2
- data/lib/bluepill/system/usage.rb +20 -0
- data/lib/bluepill/version.rb +11 -11
- data/sig/bluepill/dsl.rbs +4 -0
- data/sig/bluepill/system.rbs +48 -13
- data/sig/bluepill.rbs +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0208aabbcc97f59a0e1186f9d01d58b96a3193b378a33341e38914f5817e2dde'
|
|
4
|
+
data.tar.gz: a985b9962e81fcce8a800e3154a971fcf8864cf43905f31abc91fed6cfdf0e2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
514
|
+
* Ruby 3.2 through 3.4
|
|
516
515
|
* Ruby 4.0
|
|
517
|
-
* JRuby
|
|
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"] ||
|
|
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",
|
|
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",
|
|
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,
|
|
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,
|
|
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.\
|
|
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 = "
|
|
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.
|
|
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
|
|
17
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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}`"
|
|
95
|
+
raise(NoMethodError, "Unknown command `#{command}`")
|
|
98
96
|
end
|
|
99
97
|
|
|
100
98
|
# Log an error that answering a client raised, with its backtrace
|
data/lib/bluepill/application.rb
CHANGED
|
@@ -148,7 +148,7 @@ module Bluepill
|
|
|
148
148
|
# @return [Object] what the block returns
|
|
149
149
|
#
|
|
150
150
|
# @api private
|
|
151
|
-
def synchronize(&
|
|
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,
|
|
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]
|
|
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,
|
|
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
|
-
|
|
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
|
data/lib/bluepill/controller.rb
CHANGED
|
@@ -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,
|
|
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)
|
|
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
|
data/lib/bluepill/daemonize.rb
CHANGED
|
@@ -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(&
|
|
93
|
-
instance_eval(&
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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, &
|
|
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(&
|
|
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)
|
data/lib/bluepill/logger.rb
CHANGED
|
@@ -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
|
-
|
|
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(",")}.
|
|
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
|
|
data/lib/bluepill/process/pid.rb
CHANGED
|
@@ -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,
|
|
114
|
+
raise(ConfigError,
|
|
115
|
+
"Config Error: Invalid stop_signals! Expected #{label} at position #{i} instead of '#{value}'.")
|
|
115
116
|
end
|
|
116
|
-
|
|
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:
|
|
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(&
|
|
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:
|
|
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(&
|
|
70
|
+
def each(&) = @watches.each(&)
|
|
71
71
|
|
|
72
72
|
# Forget what the watches have read, as when the process changes state
|
|
73
73
|
#
|
data/lib/bluepill/process.rb
CHANGED
|
@@ -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:
|
|
232
|
-
@triggers = Triggers.new(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
|
|
38
|
-
environment
|
|
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, &
|
|
55
|
-
Timeout.timeout(secs, &
|
|
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:
|
|
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)
|
|
@@ -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,
|
|
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
|