dante 0.1.5 → 0.3.0
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 +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.github/workflows/release.yml +210 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +1 -1
- data/LICENSE +2 -2
- data/README.md +23 -3
- data/Rakefile +15 -7
- data/dante.gemspec +4 -5
- data/lib/dante/runner.rb +186 -45
- data/lib/dante/version.rb +1 -1
- data/test/dante_test.rb +3 -3
- data/test/runner_test.rb +683 -57
- data/test/test_helper.rb +142 -12
- data/test/user_switch_test.rb +194 -0
- metadata +52 -85
data/test/test_helper.rb
CHANGED
|
@@ -1,24 +1,144 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
|
+
require 'fileutils'
|
|
2
3
|
require 'tempfile'
|
|
4
|
+
require 'tmpdir'
|
|
3
5
|
require 'minitest/autorun'
|
|
4
|
-
|
|
5
|
-
$:.unshift File.expand_path("../../lib")
|
|
6
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
|
6
7
|
require 'dante'
|
|
7
8
|
|
|
9
|
+
module DanteTestHarness
|
|
10
|
+
TEST_PID = Process.pid
|
|
11
|
+
|
|
12
|
+
# The signal handlers in place before any test ran; Runner#start replaces them in
|
|
13
|
+
# the test process itself whenever a test runs a runner in the foreground.
|
|
14
|
+
ORIGINAL_TRAPS = %w(INT TERM).each_with_object({}) do |sig, traps|
|
|
15
|
+
traps[sig] = trap(sig, 'DEFAULT')
|
|
16
|
+
trap(sig, traps[sig])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Daemons are forked from inside a test, so every forked process inherits the at_exit
|
|
20
|
+
# handlers of the test process (minitest's among them). Make forked processes leave
|
|
21
|
+
# without running them. It is registered during the run, after minitest's own handler,
|
|
22
|
+
# so it runs first.
|
|
23
|
+
def self.install_fork_guard
|
|
24
|
+
return if @fork_guard_installed
|
|
25
|
+
@fork_guard_installed = true
|
|
26
|
+
at_exit do
|
|
27
|
+
next if Process.pid == TEST_PID
|
|
28
|
+
exit!($!.is_a?(SystemExit) ? $!.status : ($! ? 1 : 0))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def before_setup
|
|
33
|
+
super
|
|
34
|
+
DanteTestHarness.install_fork_guard
|
|
35
|
+
@tmpdir = Dir.mktmpdir('dante-test-')
|
|
36
|
+
@original_argv = ARGV.dup
|
|
37
|
+
ARGV.clear
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def after_teardown
|
|
41
|
+
stop_daemons
|
|
42
|
+
ensure
|
|
43
|
+
ARGV.replace(@original_argv)
|
|
44
|
+
ORIGINAL_TRAPS.each { |sig, handler| trap(sig, handler) }
|
|
45
|
+
FileUtils.rm_rf(@tmpdir)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A path inside this test's own temporary directory.
|
|
50
|
+
def tmp_path(name)
|
|
51
|
+
File.join(@tmpdir, name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Polls until the block returns a truthy value and returns it, or returns nil once
|
|
55
|
+
# timeout seconds have passed.
|
|
56
|
+
def wait_until(timeout = 10, interval = 0.05)
|
|
57
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
58
|
+
loop do
|
|
59
|
+
result = yield
|
|
60
|
+
return result if result
|
|
61
|
+
return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
62
|
+
sleep interval
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The pid of the daemon recorded in pid_path.
|
|
67
|
+
def daemon_pid(pid_path)
|
|
68
|
+
pid = wait_until { File.exist?(pid_path) && (n = File.read(pid_path).to_i) > 0 && n }
|
|
69
|
+
assert pid, "no pid written to #{pid_path}"
|
|
70
|
+
(@daemon_pids ||= []) << pid # stopped in teardown even if the pid file is gone by then
|
|
71
|
+
pid
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Waits until a daemon has reached its startup block, shown by it creating path (and
|
|
75
|
+
# writing pattern to it). Runner#daemonize returns once the pid file exists, before the
|
|
76
|
+
# daemon has run its startup block.
|
|
77
|
+
def wait_for_output(path, pattern = //)
|
|
78
|
+
assert wait_until { File.exist?(path) && File.read(path) =~ pattern }, "#{path} never matched #{pattern.inspect}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Waits for a daemon to exit (it is not our child: init reaps it).
|
|
82
|
+
def wait_for_exit(pid, timeout = 10)
|
|
83
|
+
assert wait_until(timeout) { !process_alive?(pid) }, "process #{pid} still running after #{timeout}s"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Whether pid is a live process. A zombie counts as gone: it has exited and only waits
|
|
87
|
+
# for its parent (init, for a daemon) to reap it.
|
|
88
|
+
# ceiling: zombies are recognised through /proc (Linux) only; elsewhere a zombie counts
|
|
89
|
+
# as alive until init reaps it, which only makes the waits longer.
|
|
90
|
+
def process_alive?(pid)
|
|
91
|
+
Process.kill(0, pid)
|
|
92
|
+
stat = "/proc/#{pid}/stat"
|
|
93
|
+
!(File.exist?(stat) && (line = File.read(stat))[line.rindex(')') + 2] == 'Z')
|
|
94
|
+
rescue Errno::ESRCH, Errno::EPERM, Errno::ENOENT, RangeError # ENOENT: reaped between the two checks; RangeError: no pid_t
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Stops every daemon whose pid file is in this test's directory, or whose pid the test
|
|
99
|
+
# read with daemon_pid, even when the test failed: SIGTERM first, then SIGKILL for
|
|
100
|
+
# daemons that trap SIGTERM.
|
|
101
|
+
# ceiling: a pid file or recorded pid of a daemon that already exited is trusted while
|
|
102
|
+
# its pid is alive, so a pid reused within the test's lifetime would be signalled;
|
|
103
|
+
# checking the process start time in /proc would rule that out.
|
|
104
|
+
def stop_daemons
|
|
105
|
+
pid_files = Dir[File.join(@tmpdir, '**', '*.pid')]
|
|
106
|
+
pids = pid_files.map { |f| [File.read(f).to_i, f] } + Array(@daemon_pids).map { |p| [p, 'daemon_pid'] }
|
|
107
|
+
pids.uniq { |pid, _| pid }.each do |pid, pid_file|
|
|
108
|
+
begin
|
|
109
|
+
next unless pid > 0 && process_alive?(pid)
|
|
110
|
+
%w(TERM KILL).each do |sig|
|
|
111
|
+
Process.kill(sig, pid)
|
|
112
|
+
break if wait_until(5) { !process_alive?(pid) }
|
|
113
|
+
end
|
|
114
|
+
rescue Errno::ESRCH
|
|
115
|
+
next
|
|
116
|
+
end
|
|
117
|
+
raise "daemon #{pid} from #{pid_file} survived SIGKILL" if process_alive?(pid)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Minitest::Test.send(:include, DanteTestHarness)
|
|
123
|
+
|
|
8
124
|
## Kernel Extensions
|
|
9
|
-
require 'stringio'
|
|
10
125
|
|
|
11
126
|
module Kernel
|
|
12
|
-
# Redirect standard out
|
|
127
|
+
# Redirect standard out and standard error to a file and return what was written
|
|
13
128
|
# capture_stdout { any_commands; you_want } => "all output from the commands"
|
|
129
|
+
#
|
|
130
|
+
# The file is a real IO rather than a StringIO so that a daemon forked inside the block
|
|
131
|
+
# reopens $stdout onto its log file, as it does outside the tests.
|
|
14
132
|
def capture_stdout
|
|
15
133
|
return yield if ENV['DEBUG'] # Skip if debug mode
|
|
16
134
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
135
|
+
Tempfile.create('stdout', @tmpdir || Dir.tmpdir) do |out|
|
|
136
|
+
out.sync = true
|
|
137
|
+
$stdout = out
|
|
138
|
+
$stderr = out
|
|
139
|
+
yield
|
|
140
|
+
File.read(out.path)
|
|
141
|
+
end
|
|
22
142
|
ensure
|
|
23
143
|
$stdout = STDOUT
|
|
24
144
|
$stderr = STDERR
|
|
@@ -29,8 +149,8 @@ end
|
|
|
29
149
|
class TestingProcess
|
|
30
150
|
attr_reader :tmp_path
|
|
31
151
|
|
|
32
|
-
def initialize(name)
|
|
33
|
-
@tmp_path = "
|
|
152
|
+
def initialize(name, dir)
|
|
153
|
+
@tmp_path = File.join(dir, "dante-#{name}.log")
|
|
34
154
|
end # initialize
|
|
35
155
|
|
|
36
156
|
def run_a!(data=nil)
|
|
@@ -64,4 +184,14 @@ class TestingProcess
|
|
|
64
184
|
ensure
|
|
65
185
|
puts "Closing!!"
|
|
66
186
|
end
|
|
67
|
-
|
|
187
|
+
|
|
188
|
+
# Survives SIGTERM (and SIGINT): only SIGKILL stops it
|
|
189
|
+
def run_d!(data=nil)
|
|
190
|
+
p ['The rescuer']
|
|
191
|
+
sleep 100
|
|
192
|
+
rescue SystemExit, SignalException
|
|
193
|
+
p ['Saved']
|
|
194
|
+
retry
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
end # TestingProcess
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
|
2
|
+
require 'etc'
|
|
3
|
+
|
|
4
|
+
# Switching user and group needs root. CI runs this file a second time under sudo.
|
|
5
|
+
describe "dante runner switching user and group" do
|
|
6
|
+
before do
|
|
7
|
+
skip "switching user and group needs root (CI runs this file under sudo)" unless Process.euid == 0
|
|
8
|
+
skip "reads credentials from /proc" unless File.exist?("/proc/self/status")
|
|
9
|
+
@nobody = begin
|
|
10
|
+
Etc.getpwnam('nobody')
|
|
11
|
+
rescue ArgumentError
|
|
12
|
+
skip "no 'nobody' user"
|
|
13
|
+
end
|
|
14
|
+
@nobody_group = Etc.getgrgid(@nobody.gid).name
|
|
15
|
+
|
|
16
|
+
# The switched process must be able to write its pid and log files.
|
|
17
|
+
File.chmod(0755, @tmpdir)
|
|
18
|
+
@dir = tmp_path('shared')
|
|
19
|
+
Dir.mkdir(@dir)
|
|
20
|
+
File.chmod(0777, @dir)
|
|
21
|
+
@pid_path = File.join(@dir, 'dante.pid')
|
|
22
|
+
@log_path = File.join(@dir, 'dante.log')
|
|
23
|
+
@status_path = File.join(@dir, 'status')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Runs the block in a forked child that starts with the given supplementary groups,
|
|
27
|
+
# as a process started by root might have. Fails the test if the block raises. The
|
|
28
|
+
# error goes through a file rather than a pipe, which a daemon started by the block
|
|
29
|
+
# would hold open.
|
|
30
|
+
def run_in_child(groups = [0, 4, 27])
|
|
31
|
+
error_path = tmp_path('child-error')
|
|
32
|
+
pid = fork do
|
|
33
|
+
error = File.open(error_path, 'w') # opened before the block switches user
|
|
34
|
+
error.sync = true # exit! below doesn't flush
|
|
35
|
+
begin
|
|
36
|
+
$stdout = $stderr = File.open(tmp_path('child.out'), 'w')
|
|
37
|
+
Process.groups = groups
|
|
38
|
+
yield
|
|
39
|
+
rescue Exception => e
|
|
40
|
+
error.write("#{e.class}: #{e.message}")
|
|
41
|
+
exit!(1)
|
|
42
|
+
end
|
|
43
|
+
exit!(0)
|
|
44
|
+
end
|
|
45
|
+
Process.wait(pid)
|
|
46
|
+
assert_equal [0, ""], [$?.exitstatus, File.read(error_path)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The real, effective, saved and filesystem uids and gids and the supplementary
|
|
50
|
+
# groups, from the text of /proc/<pid>/status.
|
|
51
|
+
def credentials(status)
|
|
52
|
+
fields = status.lines.map { |line| line.chomp.split(/:\s*/, 2) }.to_h
|
|
53
|
+
{ :uid => fields['Uid'].split.map(&:to_i), :gid => fields['Gid'].split.map(&:to_i),
|
|
54
|
+
:groups => fields['Groups'].to_s.split.map(&:to_i).sort }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The supplementary groups the group database gives user, with primary group gid.
|
|
58
|
+
def groups_of(user, gid)
|
|
59
|
+
gids = [gid]
|
|
60
|
+
Etc.group { |group| gids << group.gid if group.mem.include?(user) }
|
|
61
|
+
gids.uniq.sort
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A uid with no passwd entry.
|
|
65
|
+
def unused_uid
|
|
66
|
+
(4242..60000).find do |uid|
|
|
67
|
+
begin
|
|
68
|
+
Etc.getpwuid(uid)
|
|
69
|
+
false
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The startup block of a foreground runner: records the process's credentials.
|
|
77
|
+
def record_status
|
|
78
|
+
status_path = @status_path
|
|
79
|
+
lambda { |_opts| File.write(status_path, File.read('/proc/self/status')) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "gives a daemon started with -u the user's uid, primary group and groups" do
|
|
83
|
+
run_in_child do
|
|
84
|
+
ARGV.replace(['-d', '-P', @pid_path, '-l', @log_path, '-u', 'nobody'])
|
|
85
|
+
Dante::Runner.new('test-process').execute { sleep 100 }
|
|
86
|
+
end
|
|
87
|
+
creds = credentials(File.read("/proc/#{daemon_pid(@pid_path)}/status"))
|
|
88
|
+
|
|
89
|
+
assert_equal [@nobody.uid] * 4, creds[:uid]
|
|
90
|
+
assert_equal [@nobody.gid] * 4, creds[:gid]
|
|
91
|
+
assert_equal groups_of('nobody', @nobody.gid), creds[:groups]
|
|
92
|
+
assert_empty creds[:groups] & [0, 4, 27]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "gives a daemon started with -u and -G that group and the user's groups" do
|
|
96
|
+
group = begin
|
|
97
|
+
Etc.getgrgid(1)
|
|
98
|
+
rescue ArgumentError
|
|
99
|
+
skip "no group with gid 1"
|
|
100
|
+
end
|
|
101
|
+
run_in_child do
|
|
102
|
+
ARGV.replace(['-d', '-P', @pid_path, '-l', @log_path, '-u', 'nobody', '-G', group.name])
|
|
103
|
+
Dante::Runner.new('test-process').execute { sleep 100 }
|
|
104
|
+
end
|
|
105
|
+
creds = credentials(File.read("/proc/#{daemon_pid(@pid_path)}/status"))
|
|
106
|
+
|
|
107
|
+
assert_equal [@nobody.uid] * 4, creds[:uid]
|
|
108
|
+
assert_equal [group.gid] * 4, creds[:gid]
|
|
109
|
+
assert_equal groups_of('nobody', group.gid), creds[:groups]
|
|
110
|
+
assert_empty creds[:groups] & [0, 4, 27]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "keeps root's uid and leaves only the new group with -G alone" do
|
|
114
|
+
run_in_child do
|
|
115
|
+
ARGV.replace(['-G', @nobody_group])
|
|
116
|
+
Dante::Runner.new('test-process').execute(&record_status)
|
|
117
|
+
end
|
|
118
|
+
creds = credentials(File.read(@status_path))
|
|
119
|
+
|
|
120
|
+
assert_equal [0] * 4, creds[:uid]
|
|
121
|
+
assert_equal [@nobody.gid] * 4, creds[:gid]
|
|
122
|
+
assert_equal [@nobody.gid], creds[:groups]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "accepts a uid, and gives it the user's primary group and groups" do
|
|
126
|
+
run_in_child do
|
|
127
|
+
Dante::Runner.new('test-process').execute(:user => @nobody.uid, &record_status)
|
|
128
|
+
end
|
|
129
|
+
creds = credentials(File.read(@status_path))
|
|
130
|
+
|
|
131
|
+
assert_equal [@nobody.uid] * 4, creds[:uid]
|
|
132
|
+
assert_equal [@nobody.gid] * 4, creds[:gid]
|
|
133
|
+
assert_equal groups_of('nobody', @nobody.gid), creds[:groups]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "keeps the group of a uid with no passwd entry and leaves only that group" do
|
|
137
|
+
uid = unused_uid
|
|
138
|
+
run_in_child do
|
|
139
|
+
Dante::Runner.new('test-process').execute(:user => uid, &record_status)
|
|
140
|
+
end
|
|
141
|
+
creds = credentials(File.read(@status_path))
|
|
142
|
+
|
|
143
|
+
assert_equal [uid] * 4, creds[:uid]
|
|
144
|
+
assert_equal [0] * 4, creds[:gid]
|
|
145
|
+
assert_equal [0], creds[:groups]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "gives a uid with no passwd entry the group named by :group" do
|
|
149
|
+
uid = unused_uid
|
|
150
|
+
run_in_child do
|
|
151
|
+
Dante::Runner.new('test-process').execute(:user => uid, :group => @nobody.gid, &record_status)
|
|
152
|
+
end
|
|
153
|
+
creds = credentials(File.read(@status_path))
|
|
154
|
+
|
|
155
|
+
assert_equal [uid] * 4, creds[:uid]
|
|
156
|
+
assert_equal [@nobody.gid] * 4, creds[:gid]
|
|
157
|
+
assert_equal [@nobody.gid], creds[:groups]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "keeps the current group when not root and given only the current user" do
|
|
161
|
+
group = begin
|
|
162
|
+
Etc.getgrgid(1)
|
|
163
|
+
rescue ArgumentError
|
|
164
|
+
skip "no group with gid 1"
|
|
165
|
+
end
|
|
166
|
+
run_in_child do
|
|
167
|
+
Dante::Runner.new('test-process').execute(:user => 'nobody', :group => group.gid) {}
|
|
168
|
+
# now nobody, with a group other than nobody's primary group
|
|
169
|
+
Dante::Runner.new('test-process').execute(:user => 'nobody', &record_status)
|
|
170
|
+
end
|
|
171
|
+
creds = credentials(File.read(@status_path))
|
|
172
|
+
|
|
173
|
+
assert_equal [@nobody.uid] * 4, creds[:uid]
|
|
174
|
+
assert_equal [group.gid] * 4, creds[:gid]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "gives a user its supplementary groups from the group database" do
|
|
178
|
+
member = nil
|
|
179
|
+
Etc.group do |group|
|
|
180
|
+
member ||= group.mem.find { |name| name != 'root' && (Etc.getpwnam(name) rescue nil) }
|
|
181
|
+
end
|
|
182
|
+
skip "no user is a member of a group in the group database" unless member
|
|
183
|
+
passwd = Etc.getpwnam(member)
|
|
184
|
+
run_in_child([0]) do
|
|
185
|
+
Dante::Runner.new('test-process').execute(:user => member, &record_status)
|
|
186
|
+
end
|
|
187
|
+
creds = credentials(File.read(@status_path))
|
|
188
|
+
|
|
189
|
+
assert_equal [passwd.uid] * 4, creds[:uid]
|
|
190
|
+
assert_equal [passwd.gid] * 4, creds[:gid]
|
|
191
|
+
assert_equal groups_of(member, passwd.gid), creds[:groups]
|
|
192
|
+
assert_operator creds[:groups].size, :>, 1
|
|
193
|
+
end
|
|
194
|
+
end
|
metadata
CHANGED
|
@@ -1,75 +1,54 @@
|
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dante
|
|
3
|
-
version: !ruby/object:Gem::Version
|
|
4
|
-
|
|
5
|
-
prerelease:
|
|
6
|
-
segments:
|
|
7
|
-
- 0
|
|
8
|
-
- 1
|
|
9
|
-
- 5
|
|
10
|
-
version: 0.1.5
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
11
5
|
platform: ruby
|
|
12
|
-
authors:
|
|
6
|
+
authors:
|
|
13
7
|
- Nathan Esquenazi
|
|
14
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
15
9
|
bindir: bin
|
|
16
10
|
cert_chain: []
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- !ruby/object:Gem::Dependency
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
21
14
|
name: rake
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
none: false
|
|
25
|
-
requirements:
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
26
17
|
- - ">="
|
|
27
|
-
- !ruby/object:Gem::Version
|
|
28
|
-
|
|
29
|
-
segments:
|
|
30
|
-
- 0
|
|
31
|
-
version: "0"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
32
20
|
type: :development
|
|
33
|
-
version_requirements: *id001
|
|
34
|
-
- !ruby/object:Gem::Dependency
|
|
35
|
-
name: minitest
|
|
36
21
|
prerelease: false
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
40
31
|
- - ">="
|
|
41
|
-
- !ruby/object:Gem::Version
|
|
42
|
-
|
|
43
|
-
segments:
|
|
44
|
-
- 0
|
|
45
|
-
version: "0"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.0'
|
|
46
34
|
type: :development
|
|
47
|
-
version_requirements: *id002
|
|
48
|
-
- !ruby/object:Gem::Dependency
|
|
49
|
-
name: mocha
|
|
50
35
|
prerelease: false
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
requirements:
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
54
38
|
- - ">="
|
|
55
|
-
- !ruby/object:Gem::Version
|
|
56
|
-
|
|
57
|
-
segments:
|
|
58
|
-
- 0
|
|
59
|
-
version: "0"
|
|
60
|
-
type: :development
|
|
61
|
-
version_requirements: *id003
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.0'
|
|
62
41
|
description: Turn any process into a demon.
|
|
63
|
-
email:
|
|
42
|
+
email:
|
|
64
43
|
- nesquena@gmail.com
|
|
65
44
|
executables: []
|
|
66
|
-
|
|
67
45
|
extensions: []
|
|
68
|
-
|
|
69
46
|
extra_rdoc_files: []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- .
|
|
47
|
+
files:
|
|
48
|
+
- ".github/workflows/ci.yml"
|
|
49
|
+
- ".github/workflows/release.yml"
|
|
50
|
+
- ".gitignore"
|
|
51
|
+
- CHANGELOG.md
|
|
73
52
|
- Gemfile
|
|
74
53
|
- LICENSE
|
|
75
54
|
- README.md
|
|
@@ -81,40 +60,28 @@ files:
|
|
|
81
60
|
- test/dante_test.rb
|
|
82
61
|
- test/runner_test.rb
|
|
83
62
|
- test/test_helper.rb
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
- test/user_switch_test.rb
|
|
64
|
+
homepage: https://github.com/nesquena/dante
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata: {}
|
|
68
|
+
post_install_message:
|
|
88
69
|
rdoc_options: []
|
|
89
|
-
|
|
90
|
-
require_paths:
|
|
70
|
+
require_paths:
|
|
91
71
|
- lib
|
|
92
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
-
|
|
94
|
-
requirements:
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
95
74
|
- - ">="
|
|
96
|
-
- !ruby/object:Gem::Version
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
version: "0"
|
|
101
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
-
none: false
|
|
103
|
-
requirements:
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '2.7'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
104
79
|
- - ">="
|
|
105
|
-
- !ruby/object:Gem::Version
|
|
106
|
-
|
|
107
|
-
segments:
|
|
108
|
-
- 0
|
|
109
|
-
version: "0"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
110
82
|
requirements: []
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
signing_key:
|
|
115
|
-
specification_version: 3
|
|
83
|
+
rubygems_version: 3.5.22
|
|
84
|
+
signing_key:
|
|
85
|
+
specification_version: 4
|
|
116
86
|
summary: Turn any process into a demon
|
|
117
|
-
test_files:
|
|
118
|
-
- test/dante_test.rb
|
|
119
|
-
- test/runner_test.rb
|
|
120
|
-
- test/test_helper.rb
|
|
87
|
+
test_files: []
|