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
@@ -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, &block) = UNIXSocket.open(socket_path(base_dir, name), &block)
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, stdout: nil, stderr: nil, timeout: 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: 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: uid, gid: gid, supplementary_groups: supplementary_groups, working_dir: working_dir,
130
- environment: environment, stdin: stdin, stdout: stdout, stderr: stderr, timeout: timeout
130
+ uid:, gid:, supplementary_groups:, working_dir:,
131
+ environment:, stdin:, stdout:, stderr:, timeout:
131
132
  }
132
133
  end
133
134
  end
@@ -67,7 +67,7 @@ module Bluepill
67
67
  # @return [Hash{Symbol => Object}] the :exit_code, :stdout and :stderr
68
68
  #
69
69
  # @api private
70
- def to_h = {exit_code: exit_code, stdout: stdout, stderr: stderr}
70
+ def to_h = {exit_code:, stdout:, stderr:}
71
71
  end
72
72
  end
73
73
  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: pid_file, logger: logger)
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
@@ -4,7 +4,7 @@ module Bluepill
4
4
  # The version of bluepill
5
5
  #
6
6
  # @example
7
- # Bluepill::Version.to_s # => "0.3.0"
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 # => 0
34
+ # Bluepill::Version.patch # => 2
35
35
  #
36
36
  # @return [Integer] the patch version
37
37
  #
38
38
  # @api public
39
- def self.patch = 0
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: 0, pre: nil}
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: major,
64
- minor: minor,
65
- patch: patch,
66
- pre: 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, 0]
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.0"
83
+ # Bluepill::Version.to_s # => "0.3.2"
84
84
  #
85
- # @return [String] the version, such as "0.3.0"
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?