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
data/lib/bluepill/socket.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Bluepill
|
|
|
27
27
|
# @return [Object] what the block returns
|
|
28
28
|
#
|
|
29
29
|
# @api private
|
|
30
|
-
def self.client(base_dir, name, &
|
|
30
|
+
def self.client(base_dir, name, &) = UNIXSocket.open(socket_path(base_dir, name), &)
|
|
31
31
|
|
|
32
32
|
# Send a command to an application's daemon, and return its answer
|
|
33
33
|
#
|
|
@@ -92,7 +92,8 @@ module Bluepill
|
|
|
92
92
|
# @return [void]
|
|
93
93
|
#
|
|
94
94
|
# @api private
|
|
95
|
-
def initialize(uid: nil, gid: nil, supplementary_groups: nil, working_dir: nil, environment: nil, stdin: nil,
|
|
95
|
+
def initialize(uid: nil, gid: nil, supplementary_groups: nil, working_dir: nil, environment: nil, stdin: nil,
|
|
96
|
+
stdout: nil, stderr: nil, timeout: nil)
|
|
96
97
|
@uid = uid
|
|
97
98
|
@gid = gid
|
|
98
99
|
@supplementary_groups = supplementary_groups
|
|
@@ -116,7 +117,7 @@ module Bluepill
|
|
|
116
117
|
#
|
|
117
118
|
# @api private
|
|
118
119
|
def for_hook(env, timeout)
|
|
119
|
-
self.class.new(**to_h, environment: environment.merge(env), timeout:
|
|
120
|
+
self.class.new(**to_h, environment: environment.merge(env), timeout:)
|
|
120
121
|
end
|
|
121
122
|
|
|
122
123
|
# Return the options as a Hash
|
|
@@ -126,8 +127,8 @@ module Bluepill
|
|
|
126
127
|
# @api private
|
|
127
128
|
def to_h
|
|
128
129
|
{
|
|
129
|
-
uid
|
|
130
|
-
environment
|
|
130
|
+
uid:, gid:, supplementary_groups:, working_dir:,
|
|
131
|
+
environment:, stdin:, stdout:, stderr:, timeout:
|
|
131
132
|
}
|
|
132
133
|
end
|
|
133
134
|
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "bluepill/system/command_result"
|
|
5
|
+
|
|
6
|
+
module Bluepill
|
|
7
|
+
module System
|
|
8
|
+
# Start commands without forking bluepill, in a Ruby that can't fork,
|
|
9
|
+
# such as JRuby
|
|
10
|
+
#
|
|
11
|
+
# The command gets the working directory and environment that a forked
|
|
12
|
+
# one sets up. It can't switch to another user or groups, which a
|
|
13
|
+
# config that sets them fails to load for.
|
|
14
|
+
#
|
|
15
|
+
# Commands and Daemon include it, and System extends them, so these are
|
|
16
|
+
# System's module functions.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module CommandSpawn
|
|
20
|
+
# The seconds between checks of whether a spawned command has exited
|
|
21
|
+
EXIT_POLL_INTERVAL = 0.05
|
|
22
|
+
|
|
23
|
+
# Start a command, without waiting for it to finish
|
|
24
|
+
#
|
|
25
|
+
# @param [String] cmd the command, split into words without a shell
|
|
26
|
+
# @param [CommandOptions] options its working_dir and environment
|
|
27
|
+
# @param [String, nil] chdir the directory to run in without a
|
|
28
|
+
# working_dir, or nil for bluepill's
|
|
29
|
+
# @param [Hash{Symbol => Object}] redirects more of Process.spawn's
|
|
30
|
+
# options, such as where its standard streams go
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer] the command's pid
|
|
33
|
+
#
|
|
34
|
+
# @raise [SystemCallError] if it can't be run, say because it doesn't
|
|
35
|
+
# exist
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
def spawn_command(cmd, options, chdir: nil, **redirects)
|
|
39
|
+
dir = options.working_dir&.to_s || chdir
|
|
40
|
+
redirects[:chdir] = dir if dir
|
|
41
|
+
program, *args = Shellwords.shellwords(cmd)
|
|
42
|
+
::Process.spawn(spawn_environment(options), program.to_s, *args, **redirects)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run a command until it finishes, and return what it printed
|
|
46
|
+
#
|
|
47
|
+
# It's execute_blocking, for a Ruby that can't fork. Two threads read
|
|
48
|
+
# what the command prints, so neither pipe fills up and stops it.
|
|
49
|
+
#
|
|
50
|
+
# @param [String] cmd the command, split into words without a shell
|
|
51
|
+
# @param [CommandOptions] options its working_dir, environment and
|
|
52
|
+
# timeout
|
|
53
|
+
#
|
|
54
|
+
# @return [CommandResult] what it printed, and its exit code, which is
|
|
55
|
+
# 1 if it couldn't be run
|
|
56
|
+
#
|
|
57
|
+
# @raise [Timeout::Error] if it ran longer than the timeout
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
def spawn_blocking(cmd, options)
|
|
61
|
+
out_read, out_write = IO.pipe
|
|
62
|
+
err_read, err_write = IO.pipe
|
|
63
|
+
run_spawned(cmd, options, [out_read, err_read], [out_write, err_write])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Spawn a command with its output on pipes, and wait for it to finish
|
|
67
|
+
#
|
|
68
|
+
# @param [String] cmd the command
|
|
69
|
+
# @param [CommandOptions] options as for spawn_blocking
|
|
70
|
+
# @param [Array(IO, IO)] readers the pipes' readers, for its standard
|
|
71
|
+
# output and standard error, which are closed after
|
|
72
|
+
# @param [Array(IO, IO)] writers the pipes' writers
|
|
73
|
+
#
|
|
74
|
+
# @return [CommandResult] what it printed, and its exit code, which is
|
|
75
|
+
# 1 if it couldn't be run
|
|
76
|
+
#
|
|
77
|
+
# @raise [Timeout::Error] if it ran longer than the timeout
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
def run_spawned(cmd, options, readers, writers)
|
|
81
|
+
out, err = writers
|
|
82
|
+
pid = spawn_blocking_command(cmd, options, out, err)
|
|
83
|
+
wait_for_spawned(cmd, options.timeout, pid, readers)
|
|
84
|
+
rescue SystemCallError => e
|
|
85
|
+
CommandResult.new(exit_code: 1, stderr: "Could not run #{cmd}: #{e.message}\n")
|
|
86
|
+
ensure
|
|
87
|
+
readers.each(&:close)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Spawn a command that runs until it finishes, with its output on pipes
|
|
91
|
+
#
|
|
92
|
+
# With a timeout, it runs in a process group of its own, which is
|
|
93
|
+
# killed if it runs too long. The pipes' writers are closed here, so
|
|
94
|
+
# they end when the command closes its ends.
|
|
95
|
+
#
|
|
96
|
+
# @param [String] cmd the command
|
|
97
|
+
# @param [CommandOptions] options as for spawn_blocking
|
|
98
|
+
# @param [IO] out the pipe for its standard output
|
|
99
|
+
# @param [IO] err the pipe for its standard error
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer] its pid
|
|
102
|
+
#
|
|
103
|
+
# @raise [SystemCallError] if it can't be run
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
def spawn_blocking_command(cmd, options, out, err)
|
|
107
|
+
# pgroup: nil leaves it in bluepill's process group
|
|
108
|
+
spawn_command(cmd, options, in: File::NULL, out:, err:, pgroup: (true if options.timeout))
|
|
109
|
+
ensure
|
|
110
|
+
out.close
|
|
111
|
+
err.close
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Wait for a spawned command to finish, and collect what it printed
|
|
115
|
+
#
|
|
116
|
+
# @param [String] cmd the command, for the error on a timeout
|
|
117
|
+
# @param [Numeric, ActiveSupport::Duration, nil] timeout the seconds to
|
|
118
|
+
# wait for it, or nil to wait until it finishes
|
|
119
|
+
# @param [Integer] pid its pid
|
|
120
|
+
# @param [Array(IO, IO)] pipes where to read its standard output and
|
|
121
|
+
# standard error
|
|
122
|
+
#
|
|
123
|
+
# @return [CommandResult] what it printed, and its exit code (nil if a
|
|
124
|
+
# signal ended it)
|
|
125
|
+
#
|
|
126
|
+
# @raise [Timeout::Error] if it ran longer than the timeout
|
|
127
|
+
#
|
|
128
|
+
# @api private
|
|
129
|
+
def wait_for_spawned(cmd, timeout, pid, pipes)
|
|
130
|
+
readers = pipes.collect { |pipe| read_in_background(pipe) }
|
|
131
|
+
status = wait_for_exit(pid, timeout) || kill_spawned_after(pid, timeout, cmd)
|
|
132
|
+
stdout, stderr = readers.collect(&:value)
|
|
133
|
+
CommandResult.new(exit_code: status.exitstatus, stdout:, stderr:)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Wait for a spawned command to exit, for up to a timeout
|
|
137
|
+
#
|
|
138
|
+
# It polls, since a thread blocked waiting for a process would keep
|
|
139
|
+
# JRuby from exiting until the process does.
|
|
140
|
+
#
|
|
141
|
+
# @param [Integer] pid the command's pid
|
|
142
|
+
# @param [Numeric, ActiveSupport::Duration, nil] timeout the seconds to
|
|
143
|
+
# wait, or nil to wait until it exits
|
|
144
|
+
#
|
|
145
|
+
# @return [Process::Status, nil] how it exited, or nil if it's still
|
|
146
|
+
# running at the timeout
|
|
147
|
+
#
|
|
148
|
+
# @api private
|
|
149
|
+
def wait_for_exit(pid, timeout)
|
|
150
|
+
deadline = monotonic_now + timeout.to_f if timeout
|
|
151
|
+
loop do
|
|
152
|
+
_, status = ::Process.wait2(pid, ::Process::WNOHANG)
|
|
153
|
+
return status if status
|
|
154
|
+
return if deadline && monotonic_now >= deadline
|
|
155
|
+
|
|
156
|
+
Kernel.sleep(EXIT_POLL_INTERVAL)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Kill a spawned command's process group after its timeout, and reap it
|
|
161
|
+
#
|
|
162
|
+
# @param [Integer] pid the command's pid, which leads the group
|
|
163
|
+
# @param [Numeric, ActiveSupport::Duration] timeout the seconds it was given
|
|
164
|
+
# @param [String] cmd the command, for the error
|
|
165
|
+
#
|
|
166
|
+
# @return [void] it never returns
|
|
167
|
+
#
|
|
168
|
+
# @raise [Timeout::Error] always
|
|
169
|
+
#
|
|
170
|
+
# @api private
|
|
171
|
+
def kill_spawned_after(pid, timeout, cmd)
|
|
172
|
+
kill_group_after(pid, timeout, cmd)
|
|
173
|
+
ensure
|
|
174
|
+
begin
|
|
175
|
+
::Process.wait(pid)
|
|
176
|
+
rescue Errno::ECHILD
|
|
177
|
+
# It was collected already
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Kill a command's process group, once it has run past its timeout
|
|
182
|
+
#
|
|
183
|
+
# Forked commands are killed this way too.
|
|
184
|
+
#
|
|
185
|
+
# @param [Integer] child the pid of the group's leader, which runs the
|
|
186
|
+
# command
|
|
187
|
+
# @param [Numeric, ActiveSupport::Duration] timeout the seconds it was given
|
|
188
|
+
# @param [String] cmd the command, for the error
|
|
189
|
+
#
|
|
190
|
+
# @return [void] it never returns
|
|
191
|
+
#
|
|
192
|
+
# @raise [Timeout::Error] always
|
|
193
|
+
#
|
|
194
|
+
# @api private
|
|
195
|
+
def kill_group_after(child, timeout, cmd)
|
|
196
|
+
begin
|
|
197
|
+
::Process.kill("KILL", -child)
|
|
198
|
+
rescue Errno::ESRCH
|
|
199
|
+
nil # it finished just now
|
|
200
|
+
end
|
|
201
|
+
raise Timeout::Error, "#{cmd} took longer than #{timeout} seconds"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Return the seconds on the monotonic clock, as a Float
|
|
205
|
+
#
|
|
206
|
+
# @return [Float]
|
|
207
|
+
#
|
|
208
|
+
# @api private
|
|
209
|
+
def monotonic_now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
210
|
+
|
|
211
|
+
# Read a pipe to its end in a thread
|
|
212
|
+
#
|
|
213
|
+
# @param [IO] reader the pipe
|
|
214
|
+
#
|
|
215
|
+
# @return [Thread] a thread whose value is what it read, or "" if the
|
|
216
|
+
# pipe was closed first, after a timeout
|
|
217
|
+
#
|
|
218
|
+
# @api private
|
|
219
|
+
def read_in_background(reader)
|
|
220
|
+
Thread.new do
|
|
221
|
+
reader.read
|
|
222
|
+
rescue IOError
|
|
223
|
+
""
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Return the environment variables a command gets, as Strings
|
|
228
|
+
#
|
|
229
|
+
# PWD is the working directory, as a shell sets it, unless the
|
|
230
|
+
# environment sets it too.
|
|
231
|
+
#
|
|
232
|
+
# @param [CommandOptions] options its working_dir and environment
|
|
233
|
+
#
|
|
234
|
+
# @return [Hash{String => String}]
|
|
235
|
+
#
|
|
236
|
+
# @api private
|
|
237
|
+
def spawn_environment(options)
|
|
238
|
+
working_dir = options.working_dir
|
|
239
|
+
env = options.environment.to_h { |key, value| [key.to_s, value.to_s] }
|
|
240
|
+
working_dir ? {"PWD" => working_dir.to_s}.merge(env) : env
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -6,6 +6,7 @@ require "bluepill/daemonize"
|
|
|
6
6
|
require "bluepill/system/command_options"
|
|
7
7
|
require "bluepill/system/command_result"
|
|
8
8
|
require "bluepill/system/command_setup"
|
|
9
|
+
require "bluepill/system/command_spawn"
|
|
9
10
|
require "bluepill/system/daemon"
|
|
10
11
|
|
|
11
12
|
module Bluepill
|
|
@@ -18,12 +19,14 @@ module Bluepill
|
|
|
18
19
|
# @api private
|
|
19
20
|
module Commands
|
|
20
21
|
include CommandSetup
|
|
22
|
+
include CommandSpawn
|
|
21
23
|
|
|
22
24
|
# Run a command until it finishes, and return what it printed
|
|
23
25
|
#
|
|
24
26
|
# With a timeout, the command runs in a process group of its own, and
|
|
25
27
|
# if it hasn't finished in time, that whole group is killed, so a hung
|
|
26
|
-
# command and anything it started don't keep running.
|
|
28
|
+
# command and anything it started don't keep running. A Ruby that
|
|
29
|
+
# can't fork spawns the command instead.
|
|
27
30
|
#
|
|
28
31
|
# @param [String] cmd the command, split into words without a shell
|
|
29
32
|
# @param [CommandOptions] options its uid, gid, supplementary_groups,
|
|
@@ -35,6 +38,8 @@ module Bluepill
|
|
|
35
38
|
#
|
|
36
39
|
# @api private
|
|
37
40
|
def execute_blocking(cmd, options = CommandOptions.new)
|
|
41
|
+
return spawn_blocking(cmd, options) unless Daemonize.can_fork?
|
|
42
|
+
|
|
38
43
|
rd, wr = IO.pipe
|
|
39
44
|
|
|
40
45
|
child = Daemonize.safefork
|
|
@@ -95,27 +100,6 @@ module Bluepill
|
|
|
95
100
|
reader.close
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
# Kill a command's process group, once it has run past its timeout
|
|
99
|
-
#
|
|
100
|
-
# @param [Integer] child the pid of the group's leader, which runs the
|
|
101
|
-
# command
|
|
102
|
-
# @param [Numeric, ActiveSupport::Duration] timeout the seconds it was given
|
|
103
|
-
# @param [String] cmd the command, for the error
|
|
104
|
-
#
|
|
105
|
-
# @return [void] it never returns
|
|
106
|
-
#
|
|
107
|
-
# @raise [Timeout::Error] always
|
|
108
|
-
#
|
|
109
|
-
# @api private
|
|
110
|
-
def kill_group_after(child, timeout, cmd)
|
|
111
|
-
begin
|
|
112
|
-
::Process.kill("KILL", -child)
|
|
113
|
-
rescue Errno::ESRCH
|
|
114
|
-
nil # it finished just now
|
|
115
|
-
end
|
|
116
|
-
raise Timeout::Error, "#{cmd} took longer than #{timeout} seconds"
|
|
117
|
-
end
|
|
118
|
-
|
|
119
103
|
# Run a command in a grandchild, report its result, and exit
|
|
120
104
|
#
|
|
121
105
|
# Sends the grandchild's output and exit code to the parent over
|
|
@@ -4,6 +4,7 @@ require "shellwords"
|
|
|
4
4
|
require "bluepill/daemonize"
|
|
5
5
|
require "bluepill/system/command_options"
|
|
6
6
|
require "bluepill/system/command_setup"
|
|
7
|
+
require "bluepill/system/command_spawn"
|
|
7
8
|
|
|
8
9
|
module Bluepill
|
|
9
10
|
module System
|
|
@@ -19,12 +20,14 @@ module Bluepill
|
|
|
19
20
|
# @api private
|
|
20
21
|
module Daemon
|
|
21
22
|
include CommandSetup
|
|
23
|
+
include CommandSpawn
|
|
22
24
|
|
|
23
25
|
# Start a command as a daemon, and write its pid file
|
|
24
26
|
#
|
|
25
27
|
# It forks a child that drops privileges, sets up the environment and
|
|
26
28
|
# standard streams, and daemonizes the command. The child tells the
|
|
27
|
-
# parent the daemon's pid over a pipe.
|
|
29
|
+
# parent the daemon's pid over a pipe. A Ruby that can't fork spawns
|
|
30
|
+
# the command instead.
|
|
28
31
|
#
|
|
29
32
|
# @param [String] cmd the command, split into words without a shell
|
|
30
33
|
# @param [CommandOptions] options its uid, gid, supplementary_groups,
|
|
@@ -36,6 +39,19 @@ module Bluepill
|
|
|
36
39
|
#
|
|
37
40
|
# @api private
|
|
38
41
|
def daemonize(cmd, options = CommandOptions.new, pid_file:, logger:)
|
|
42
|
+
return fork_daemon(cmd, options, pid_file:, logger:) if Daemonize.can_fork?
|
|
43
|
+
|
|
44
|
+
spawn_daemon(cmd, options, pid_file:, logger:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Start a command as a daemon by forking, and write its pid file
|
|
48
|
+
#
|
|
49
|
+
# @param (see #daemonize)
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer, nil] the daemon's pid, or nil if it didn't start
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
def fork_daemon(cmd, options, pid_file:, logger:)
|
|
39
55
|
rd, wr = IO.pipe
|
|
40
56
|
|
|
41
57
|
child = Daemonize.safefork
|
|
@@ -49,10 +65,71 @@ module Bluepill
|
|
|
49
65
|
rd.close
|
|
50
66
|
|
|
51
67
|
drop_privileges(options.uid, options.gid, options.supplementary_groups)
|
|
52
|
-
start_daemon(cmd, options, wr, pid_file
|
|
68
|
+
start_daemon(cmd, options, wr, pid_file:, logger:)
|
|
53
69
|
end
|
|
54
70
|
end
|
|
55
71
|
|
|
72
|
+
# Start a command as a daemon without forking, and write its pid file
|
|
73
|
+
#
|
|
74
|
+
# The command runs in a process group of its own, as a forked daemon's
|
|
75
|
+
# new session does, so killing what's left in it never kills bluepill.
|
|
76
|
+
# It stays bluepill's child, and pid_alive? collects its exit status.
|
|
77
|
+
#
|
|
78
|
+
# @param [String] cmd the command, split into words without a shell
|
|
79
|
+
# @param [CommandOptions] options its working_dir, environment, stdin,
|
|
80
|
+
# stdout and stderr
|
|
81
|
+
# @param [String] pid_file where to write the daemon's pid
|
|
82
|
+
# @param [Logger] logger where to log why it didn't start
|
|
83
|
+
#
|
|
84
|
+
# @return [Integer, nil] the daemon's pid, or nil if it didn't start
|
|
85
|
+
#
|
|
86
|
+
# @api private
|
|
87
|
+
def spawn_daemon(cmd, options, pid_file:, logger:)
|
|
88
|
+
return unless can_write_pid_file(pid_file, logger)
|
|
89
|
+
|
|
90
|
+
pid = spawn_command(cmd, options, chdir: "/", pgroup: true, **daemon_streams(options))
|
|
91
|
+
File.write(pid_file, pid.to_s)
|
|
92
|
+
pid
|
|
93
|
+
rescue SystemCallError => e
|
|
94
|
+
logger.warning(format("%s - %s", e.class, e))
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Return where a spawned daemon's standard streams go
|
|
99
|
+
#
|
|
100
|
+
# They're /dev/null unless the options name files. Output and error
|
|
101
|
+
# share one file when they name the same one, and are appended to.
|
|
102
|
+
#
|
|
103
|
+
# @param [CommandOptions] options its stdin, stdout and stderr
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash{Symbol => Object}] Process.spawn's in, out and err
|
|
106
|
+
#
|
|
107
|
+
# @api private
|
|
108
|
+
def daemon_streams(options)
|
|
109
|
+
out = options.stdout
|
|
110
|
+
{
|
|
111
|
+
in: options.stdin || File::NULL,
|
|
112
|
+
out: out ? [out, APPEND_MODE] : File::NULL,
|
|
113
|
+
err: daemon_stderr(out, options.stderr)
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Return where a spawned daemon's standard error goes
|
|
118
|
+
#
|
|
119
|
+
# @param [String, nil] out the file for standard output
|
|
120
|
+
# @param [String, nil] err the file for standard error
|
|
121
|
+
#
|
|
122
|
+
# @return [Object] Process.spawn's err: standard output when they're
|
|
123
|
+
# the same file, the file, or /dev/null
|
|
124
|
+
#
|
|
125
|
+
# @api private
|
|
126
|
+
def daemon_stderr(out, err)
|
|
127
|
+
return File::NULL unless err
|
|
128
|
+
return [:child, :out] if err.eql?(out)
|
|
129
|
+
|
|
130
|
+
[err, APPEND_MODE]
|
|
131
|
+
end
|
|
132
|
+
|
|
56
133
|
# Read the daemon's pid from the child that started it, in the parent
|
|
57
134
|
#
|
|
58
135
|
# @param [IO] reader the end of the pipe that the child writes the pid to
|
|
@@ -18,6 +18,7 @@ module Bluepill
|
|
|
18
18
|
#
|
|
19
19
|
# @api private
|
|
20
20
|
def pid_alive?(pid)
|
|
21
|
+
reap(pid)
|
|
21
22
|
::Process.kill(0, pid)
|
|
22
23
|
true
|
|
23
24
|
rescue Errno::EPERM # no permission, but it is definitely alive
|
|
@@ -26,6 +27,25 @@ module Bluepill
|
|
|
26
27
|
false # no such process (ESRCH), or no pid
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Collect the exit status of a daemon that bluepill spawned, if it exited
|
|
31
|
+
#
|
|
32
|
+
# A spawned daemon stays bluepill's child, and one that exited is a
|
|
33
|
+
# zombie, which kill(0) still finds, until bluepill collects its exit
|
|
34
|
+
# status. A thread that waits for it instead would keep JRuby from
|
|
35
|
+
# exiting while the daemon runs. A Ruby that forks has no daemons to
|
|
36
|
+
# collect, since init adopts them.
|
|
37
|
+
#
|
|
38
|
+
# @param [Integer, nil] pid the daemon's pid
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
def reap(pid)
|
|
44
|
+
::Process.wait(pid, ::Process::WNOHANG) if pid && !Daemonize.can_fork?
|
|
45
|
+
rescue Errno::ECHILD
|
|
46
|
+
# It isn't bluepill's child, or was collected already
|
|
47
|
+
end
|
|
48
|
+
|
|
29
49
|
# Return when a process started, in seconds since the epoch
|
|
30
50
|
#
|
|
31
51
|
# It's read from ps's elapsed time for the process, so it's accurate to
|
data/lib/bluepill/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Bluepill
|
|
|
4
4
|
# The version of bluepill
|
|
5
5
|
#
|
|
6
6
|
# @example
|
|
7
|
-
# Bluepill::Version.to_s # => "0.3.
|
|
7
|
+
# Bluepill::Version.to_s # => "0.3.2"
|
|
8
8
|
#
|
|
9
9
|
# @api public
|
|
10
10
|
module Version
|
|
@@ -31,12 +31,12 @@ module Bluepill
|
|
|
31
31
|
# Return the patch version
|
|
32
32
|
#
|
|
33
33
|
# @example
|
|
34
|
-
# Bluepill::Version.patch # =>
|
|
34
|
+
# Bluepill::Version.patch # => 2
|
|
35
35
|
#
|
|
36
36
|
# @return [Integer] the patch version
|
|
37
37
|
#
|
|
38
38
|
# @api public
|
|
39
|
-
def self.patch =
|
|
39
|
+
def self.patch = 2
|
|
40
40
|
|
|
41
41
|
# Return the pre-release version
|
|
42
42
|
#
|
|
@@ -52,7 +52,7 @@ module Bluepill
|
|
|
52
52
|
# Return the version's parts by name
|
|
53
53
|
#
|
|
54
54
|
# @example
|
|
55
|
-
# Bluepill::Version.to_h # => {major: 0, minor: 3, patch:
|
|
55
|
+
# Bluepill::Version.to_h # => {major: 0, minor: 3, patch: 2, pre: nil}
|
|
56
56
|
#
|
|
57
57
|
# @return [Hash{Symbol => Integer, nil}] the major, minor, patch and
|
|
58
58
|
# pre-release versions
|
|
@@ -60,17 +60,17 @@ module Bluepill
|
|
|
60
60
|
# @api public
|
|
61
61
|
def self.to_h
|
|
62
62
|
{
|
|
63
|
-
major
|
|
64
|
-
minor
|
|
65
|
-
patch
|
|
66
|
-
pre:
|
|
63
|
+
major:,
|
|
64
|
+
minor:,
|
|
65
|
+
patch:,
|
|
66
|
+
pre:
|
|
67
67
|
}
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Return the version's parts, without a missing pre-release version
|
|
71
71
|
#
|
|
72
72
|
# @example
|
|
73
|
-
# Bluepill::Version.to_a # => [0, 3,
|
|
73
|
+
# Bluepill::Version.to_a # => [0, 3, 2]
|
|
74
74
|
#
|
|
75
75
|
# @return [Array<Integer>] the parts of the version
|
|
76
76
|
#
|
|
@@ -80,9 +80,9 @@ module Bluepill
|
|
|
80
80
|
# Return the version as a string
|
|
81
81
|
#
|
|
82
82
|
# @example
|
|
83
|
-
# Bluepill::Version.to_s # => "0.3.
|
|
83
|
+
# Bluepill::Version.to_s # => "0.3.2"
|
|
84
84
|
#
|
|
85
|
-
# @return [String] the version, such as "0.3.
|
|
85
|
+
# @return [String] the version, such as "0.3.2"
|
|
86
86
|
#
|
|
87
87
|
# @api public
|
|
88
88
|
def self.to_s = to_a.join(".")
|
data/sig/bluepill/dsl.rbs
CHANGED
|
@@ -142,6 +142,10 @@ module Bluepill
|
|
|
142
142
|
include DSL::Attributes
|
|
143
143
|
extend DSL::Attributes::ClassMethods
|
|
144
144
|
|
|
145
|
+
PRIVILEGE_ATTRIBUTES: Array[Symbol]
|
|
146
|
+
|
|
147
|
+
def validate_privileges!: () -> void
|
|
148
|
+
|
|
145
149
|
attr_reader name: String
|
|
146
150
|
attr_reader child_process_block: process_block?
|
|
147
151
|
attr_reader actual_pid: Integer?
|