daemon_controller 1.0.0 → 1.1.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.
- data/daemon_controller.gemspec +2 -2
- data/lib/daemon_controller.rb +120 -25
- data/lib/daemon_controller/lock_file.rb +2 -2
- data/lib/daemon_controller/spawn.rb +3 -1
- data/lib/daemon_controller/version.rb +1 -1
- data/spec/daemon_controller_spec.rb +75 -39
- data/spec/echo_server.rb +18 -2
- metadata +4 -4
data/daemon_controller.gemspec
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "daemon_controller"
|
3
3
|
# Don't forget to update version.rb too.
|
4
|
-
s.version = "1.
|
5
|
-
s.date = "2012-
|
4
|
+
s.version = "1.1.0"
|
5
|
+
s.date = "2012-10-27"
|
6
6
|
s.summary = "A library for implementing daemon management capabilities"
|
7
7
|
s.email = "hongli@phusion.nl"
|
8
8
|
s.homepage = "https://github.com/FooBarWidget/daemon_controller"
|
data/lib/daemon_controller.rb
CHANGED
@@ -22,15 +22,12 @@
|
|
22
22
|
require 'tempfile'
|
23
23
|
require 'fcntl'
|
24
24
|
require 'timeout'
|
25
|
-
|
26
|
-
libdir = File.expand_path(File.dirname(__FILE__))
|
27
|
-
$LOAD_PATH.unshift(libdir)
|
28
|
-
require 'daemon_controller/lock_file'
|
29
|
-
|
30
25
|
if Process.respond_to?(:spawn)
|
31
26
|
require 'rbconfig'
|
32
27
|
end
|
33
28
|
|
29
|
+
require 'daemon_controller/lock_file'
|
30
|
+
|
34
31
|
# Main daemon controller object. See the README for an introduction and tutorial.
|
35
32
|
class DaemonController
|
36
33
|
ALLOWED_CONNECT_EXCEPTIONS = [Errno::ECONNREFUSED, Errno::ENETUNREACH,
|
@@ -123,6 +120,13 @@ class DaemonController
|
|
123
120
|
#
|
124
121
|
# The default value is +nil+.
|
125
122
|
#
|
123
|
+
# [:restart_command]
|
124
|
+
# A command to restart the daemon with, e.g. "/etc/rc.d/nginx restart". If
|
125
|
+
# no restart command is given (i.e. +nil+), then DaemonController will
|
126
|
+
# restart the daemon by calling #stop and #start.
|
127
|
+
#
|
128
|
+
# The default value is +nil+.
|
129
|
+
#
|
126
130
|
# [:before_start]
|
127
131
|
# This may be a Proc. It will be called just before running the start command.
|
128
132
|
# The before_start proc is not subject to the start timeout.
|
@@ -173,6 +177,10 @@ class DaemonController
|
|
173
177
|
# descriptors except stdin, stdout and stderr. However if there are any file
|
174
178
|
# descriptors you want to keep open, specify the IO objects here. This must be
|
175
179
|
# an array of IO objects.
|
180
|
+
#
|
181
|
+
# [:env]
|
182
|
+
# This must be a Hash. The hash will contain the environment variables available
|
183
|
+
# to be made available to the daemon. Hash keys must be strings, not symbols.
|
176
184
|
def initialize(options)
|
177
185
|
[:identifier, :start_command, :ping_command, :pid_file, :log_file].each do |option|
|
178
186
|
if !options.has_key?(option)
|
@@ -183,6 +191,7 @@ class DaemonController
|
|
183
191
|
@start_command = options[:start_command]
|
184
192
|
@stop_command = options[:stop_command]
|
185
193
|
@ping_command = options[:ping_command]
|
194
|
+
@restart_command = options[:restart_command]
|
186
195
|
@ping_interval = options[:ping_interval] || 0.1
|
187
196
|
@pid_file = options[:pid_file]
|
188
197
|
@log_file = options[:log_file]
|
@@ -193,6 +202,7 @@ class DaemonController
|
|
193
202
|
@daemonize_for_me = options[:daemonize_for_me]
|
194
203
|
@keep_ios = options[:keep_ios] || []
|
195
204
|
@lock_file = determine_lock_file(options, @identifier, @pid_file)
|
205
|
+
@env = options[:env] || {}
|
196
206
|
end
|
197
207
|
|
198
208
|
# Start the daemon and wait until it can be pinged.
|
@@ -289,6 +299,17 @@ class DaemonController
|
|
289
299
|
end
|
290
300
|
end
|
291
301
|
|
302
|
+
# Restarts the daemon. Uses the restart_command if provided, otherwise
|
303
|
+
# calls #stop and #start.
|
304
|
+
def restart
|
305
|
+
if @restart_command
|
306
|
+
run_command(@restart_command)
|
307
|
+
else
|
308
|
+
stop
|
309
|
+
start
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
292
313
|
# Returns the daemon's PID, as reported by its PID file. Returns the PID
|
293
314
|
# as an integer, or nil there is no valid PID in the PID file.
|
294
315
|
#
|
@@ -315,6 +336,12 @@ class DaemonController
|
|
315
336
|
end
|
316
337
|
end
|
317
338
|
|
339
|
+
# Checks whether ping Unix domain sockets is supported. Currently
|
340
|
+
# this is supported on all Ruby implementations, except JRuby.
|
341
|
+
def self.can_ping_unix_sockets?
|
342
|
+
return RUBY_PLATFORM != "java"
|
343
|
+
end
|
344
|
+
|
318
345
|
private
|
319
346
|
def start_without_locking
|
320
347
|
if daemon_is_running?
|
@@ -558,6 +585,11 @@ private
|
|
558
585
|
def self.fork_supported?
|
559
586
|
return RUBY_PLATFORM != "java" && RUBY_PLATFORM !~ /win32/
|
560
587
|
end
|
588
|
+
|
589
|
+
def self.spawn_supported?
|
590
|
+
# Process.spawn doesn't work very well in JRuby.
|
591
|
+
return Process.respond_to?(:spawn) && RUBY_PLATFORM != "java"
|
592
|
+
end
|
561
593
|
|
562
594
|
def run_command(command)
|
563
595
|
# Create tempfile for storing the command's output.
|
@@ -566,7 +598,7 @@ private
|
|
566
598
|
File.chmod(0666, tempfile_path)
|
567
599
|
tempfile.close
|
568
600
|
|
569
|
-
if self.class.fork_supported? ||
|
601
|
+
if self.class.fork_supported? || self.class.spawn_supported?
|
570
602
|
if Process.respond_to?(:spawn)
|
571
603
|
options = {
|
572
604
|
:in => "/dev/null",
|
@@ -578,14 +610,10 @@ private
|
|
578
610
|
options[io] = io
|
579
611
|
end
|
580
612
|
if @daemonize_for_me
|
581
|
-
|
582
|
-
Config::CONFIG['bindir'],
|
583
|
-
Config::CONFIG['RUBY_INSTALL_NAME']
|
584
|
-
) + Config::CONFIG['EXEEXT']
|
585
|
-
pid = Process.spawn(ruby_interpreter, SPAWNER_FILE,
|
613
|
+
pid = Process.spawn(@env, ruby_interpreter, SPAWNER_FILE,
|
586
614
|
command, options)
|
587
615
|
else
|
588
|
-
pid = Process.spawn(command, options)
|
616
|
+
pid = Process.spawn(@env, command, options)
|
589
617
|
end
|
590
618
|
else
|
591
619
|
pid = safe_fork(@daemonize_for_me) do
|
@@ -597,6 +625,7 @@ private
|
|
597
625
|
STDIN.reopen("/dev/null", "r")
|
598
626
|
STDOUT.reopen(tempfile_path, "w")
|
599
627
|
STDERR.reopen(tempfile_path, "w")
|
628
|
+
ENV.update(@env)
|
600
629
|
exec(command)
|
601
630
|
end
|
602
631
|
end
|
@@ -642,8 +671,14 @@ private
|
|
642
671
|
raise StartError, File.read(tempfile_path).strip
|
643
672
|
end
|
644
673
|
else
|
674
|
+
if @env && !@env.empty?
|
675
|
+
raise "Setting the :env option is not supported on this Ruby implementation."
|
676
|
+
elsif @daemonize_for_me
|
677
|
+
raise "Setting the :daemonize_for_me option is not supported on this Ruby implementation."
|
678
|
+
end
|
679
|
+
|
645
680
|
cmd = "#{command} >\"#{tempfile_path}\""
|
646
|
-
cmd
|
681
|
+
cmd << " 2>\"#{tempfile_path}\"" unless PLATFORM =~ /mswin/
|
647
682
|
if !system(cmd)
|
648
683
|
raise StartError, File.read(tempfile_path).strip
|
649
684
|
end
|
@@ -665,19 +700,74 @@ private
|
|
665
700
|
end
|
666
701
|
elsif @ping_command.is_a?(Array)
|
667
702
|
type, *args = @ping_command
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
703
|
+
if self.class.can_ping_unix_sockets?
|
704
|
+
case type
|
705
|
+
when :tcp
|
706
|
+
socket_domain = Socket::Constants::AF_INET
|
707
|
+
hostname, port = args
|
708
|
+
sockaddr = Socket.pack_sockaddr_in(port, hostname)
|
709
|
+
when :unix
|
710
|
+
socket_domain = Socket::Constants::AF_LOCAL
|
711
|
+
sockaddr = Socket.pack_sockaddr_un(args[0])
|
712
|
+
else
|
713
|
+
raise ArgumentError, "Unknown ping command type #{type.inspect}"
|
714
|
+
end
|
715
|
+
return ping_socket(socket_domain, sockaddr)
|
677
716
|
else
|
678
|
-
|
717
|
+
case type
|
718
|
+
when :tcp
|
719
|
+
hostname, port = args
|
720
|
+
return ping_socket(hostname, port)
|
721
|
+
when :unix
|
722
|
+
raise "Pinging Unix domain sockets is not supported on this Ruby implementation"
|
723
|
+
else
|
724
|
+
raise ArgumentError, "Unknown ping command type #{type.inspect}"
|
725
|
+
end
|
679
726
|
end
|
727
|
+
else
|
728
|
+
return system(@ping_command)
|
729
|
+
end
|
730
|
+
end
|
680
731
|
|
732
|
+
if !can_ping_unix_sockets?
|
733
|
+
require 'java'
|
734
|
+
|
735
|
+
def ping_socket(host_name, port)
|
736
|
+
channel = java.nio.channels.SocketChannel.open
|
737
|
+
begin
|
738
|
+
address = java.net.InetSocketAddress.new(host_name, port)
|
739
|
+
channel.configure_blocking(false)
|
740
|
+
if channel.connect(address)
|
741
|
+
return true
|
742
|
+
end
|
743
|
+
|
744
|
+
deadline = Time.now.to_f + 0.1
|
745
|
+
done = false
|
746
|
+
while true
|
747
|
+
begin
|
748
|
+
if channel.finish_connect
|
749
|
+
return true
|
750
|
+
end
|
751
|
+
rescue java.net.ConnectException => e
|
752
|
+
if e.message =~ /Connection refused/i
|
753
|
+
return false
|
754
|
+
else
|
755
|
+
throw e
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
# Not done connecting and no error.
|
760
|
+
sleep 0.01
|
761
|
+
if Time.now.to_f >= deadline
|
762
|
+
return false
|
763
|
+
end
|
764
|
+
end
|
765
|
+
ensure
|
766
|
+
channel.close
|
767
|
+
end
|
768
|
+
end
|
769
|
+
else
|
770
|
+
def ping_socket(socket_domain, sockaddr)
|
681
771
|
begin
|
682
772
|
socket = Socket.new(socket_domain, Socket::Constants::SOCK_STREAM, 0)
|
683
773
|
begin
|
@@ -698,11 +788,16 @@ private
|
|
698
788
|
ensure
|
699
789
|
socket.close if socket
|
700
790
|
end
|
701
|
-
else
|
702
|
-
return system(@ping_command)
|
703
791
|
end
|
704
792
|
end
|
705
793
|
|
794
|
+
def ruby_interpreter
|
795
|
+
File.join(
|
796
|
+
Config::CONFIG['bindir'],
|
797
|
+
Config::CONFIG['RUBY_INSTALL_NAME']
|
798
|
+
) + Config::CONFIG['EXEEXT']
|
799
|
+
end
|
800
|
+
|
706
801
|
def safe_fork(double_fork)
|
707
802
|
pid = fork
|
708
803
|
if pid.nil?
|
@@ -78,7 +78,7 @@ class LockFile
|
|
78
78
|
# The lock file *must* be writable, otherwise an Errno::EACCESS
|
79
79
|
# exception will be raised.
|
80
80
|
def shared_lock
|
81
|
-
File.open(@filename, 'w') do |f|
|
81
|
+
File.open(@filename, 'w+') do |f|
|
82
82
|
if Fcntl.const_defined? :F_SETFD
|
83
83
|
f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
84
84
|
end
|
@@ -93,7 +93,7 @@ class LockFile
|
|
93
93
|
#
|
94
94
|
# If a lock can be obtained, then the given block will be yielded.
|
95
95
|
def try_shared_lock
|
96
|
-
File.open(@filename, 'w') do |f|
|
96
|
+
File.open(@filename, 'w+') do |f|
|
97
97
|
if Fcntl.const_defined? :F_SETFD
|
98
98
|
f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
99
99
|
end
|
@@ -19,6 +19,8 @@
|
|
19
19
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
20
|
# THE SOFTWARE.
|
21
21
|
|
22
|
-
#
|
22
|
+
# This helper script is used for daemonizing a command by executing it and
|
23
|
+
# then exiting ourselves. Used on Ruby 1.9 and JRuby because forking may not
|
24
|
+
# be safe/supported on all platforms.
|
23
25
|
Process.setsid
|
24
26
|
Process.spawn(ARGV[0])
|
@@ -128,7 +128,7 @@ describe DaemonController, "#start" do
|
|
128
128
|
end
|
129
129
|
end
|
130
130
|
|
131
|
-
if DaemonController.send(:fork_supported?) ||
|
131
|
+
if DaemonController.send(:fork_supported?) || DaemonController.send(:spawn_supported?)
|
132
132
|
it "kills the daemon if it doesn't start in time and hasn't forked " <<
|
133
133
|
"yet, on platforms where Ruby supports fork() or Process.spawn" do
|
134
134
|
begin
|
@@ -209,7 +209,7 @@ describe DaemonController, "#start" do
|
|
209
209
|
log.should == ["before_start", "start_command"]
|
210
210
|
end
|
211
211
|
|
212
|
-
if DaemonController.send(:fork_supported?) ||
|
212
|
+
if DaemonController.send(:fork_supported?) || DaemonController.send(:spawn_supported?)
|
213
213
|
it "keeps the file descriptors in 'keep_ios' open" do
|
214
214
|
a, b = IO.pipe
|
215
215
|
begin
|
@@ -234,6 +234,14 @@ describe DaemonController, "#start" do
|
|
234
234
|
@controller.stop
|
235
235
|
end
|
236
236
|
end
|
237
|
+
|
238
|
+
it "receives environment variables" do
|
239
|
+
new_controller(:env => {'ENV_FILE' => 'spec/env_file.tmp'})
|
240
|
+
File.unlink('spec/env_file.tmp') if File.exist?('spec/env_file.tmp')
|
241
|
+
@controller.start
|
242
|
+
File.exist?('spec/env_file.tmp').should be_true
|
243
|
+
@controller.stop
|
244
|
+
end
|
237
245
|
end
|
238
246
|
|
239
247
|
describe DaemonController, "#stop" do
|
@@ -291,6 +299,36 @@ describe DaemonController, "#stop" do
|
|
291
299
|
end
|
292
300
|
end
|
293
301
|
|
302
|
+
describe DaemonController, "#restart" do
|
303
|
+
include TestHelper
|
304
|
+
|
305
|
+
before :each do
|
306
|
+
new_controller
|
307
|
+
end
|
308
|
+
|
309
|
+
it "raises no exception if the daemon is not running" do
|
310
|
+
@controller.restart
|
311
|
+
end
|
312
|
+
|
313
|
+
describe 'with no restart command' do
|
314
|
+
it "restart the daemon using stop and start" do
|
315
|
+
@controller.should_receive(:stop)
|
316
|
+
@controller.should_receive(:start)
|
317
|
+
@controller.restart
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
describe 'with a restart_command' do
|
322
|
+
it 'restarts the daemon using the restart_command' do
|
323
|
+
stop_cmd = "echo 'hello world'"
|
324
|
+
new_controller :restart_command => stop_cmd
|
325
|
+
|
326
|
+
@controller.should_receive(:run_command).with(stop_cmd)
|
327
|
+
@controller.restart
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
294
332
|
describe DaemonController, "#connect" do
|
295
333
|
include TestHelper
|
296
334
|
|
@@ -321,6 +359,11 @@ end
|
|
321
359
|
|
322
360
|
describe DaemonController do
|
323
361
|
include TestHelper
|
362
|
+
|
363
|
+
after :each do
|
364
|
+
@server.close if @server && !@server.closed?
|
365
|
+
File.unlink('spec/foo.sock') rescue nil
|
366
|
+
end
|
324
367
|
|
325
368
|
specify "if the ping command is a block that raises Errno::ECONNREFUSED, then that's " <<
|
326
369
|
"an indication that the daemon cannot be connected to" do
|
@@ -332,57 +375,50 @@ describe DaemonController do
|
|
332
375
|
|
333
376
|
specify "if the ping command is a block that returns an object that responds to #close, " <<
|
334
377
|
"then the close method will be called on that object" do
|
335
|
-
server = TCPServer.new('localhost', 8278)
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
socket.should be_closed
|
343
|
-
ensure
|
344
|
-
server.close
|
345
|
-
end
|
378
|
+
@server = TCPServer.new('localhost', 8278)
|
379
|
+
socket = nil
|
380
|
+
new_controller(:ping_command => lambda do
|
381
|
+
socket = TCPSocket.new('localhost', 8278)
|
382
|
+
end)
|
383
|
+
@controller.send(:run_ping_command)
|
384
|
+
socket.should be_closed
|
346
385
|
end
|
347
386
|
|
348
387
|
specify "if the ping command is a block that returns an object that responds to #close, " <<
|
349
388
|
"and #close raises an exception, then that exception is ignored" do
|
350
|
-
server = TCPServer.new('localhost', 8278)
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
raise StandardError, "foo"
|
355
|
-
end
|
356
|
-
new_controller(:ping_command => lambda do
|
357
|
-
o
|
358
|
-
end)
|
359
|
-
lambda { @controller.send(:run_ping_command) }.should_not raise_error(StandardError)
|
360
|
-
ensure
|
361
|
-
server.close
|
389
|
+
@server = TCPServer.new('localhost', 8278)
|
390
|
+
o = Object.new
|
391
|
+
o.should_receive(:close).and_return do
|
392
|
+
raise StandardError, "foo"
|
362
393
|
end
|
394
|
+
new_controller(:ping_command => lambda do
|
395
|
+
o
|
396
|
+
end)
|
397
|
+
lambda { @controller.send(:run_ping_command) }.should_not raise_error(StandardError)
|
363
398
|
end
|
364
399
|
|
365
400
|
specify "the ping command may be [:tcp, hostname, port]" do
|
366
|
-
new_controller(:ping_command => [:tcp, "
|
401
|
+
new_controller(:ping_command => [:tcp, "127.0.0.1", 8278])
|
367
402
|
@controller.send(:run_ping_command).should be_false
|
368
403
|
|
369
|
-
server = TCPServer.new('
|
370
|
-
|
371
|
-
@controller.send(:run_ping_command).should be_true
|
372
|
-
ensure
|
373
|
-
server.close
|
374
|
-
end
|
404
|
+
@server = TCPServer.new('127.0.0.1', 8278)
|
405
|
+
@controller.send(:run_ping_command).should be_true
|
375
406
|
end
|
376
407
|
|
377
|
-
|
378
|
-
|
379
|
-
|
408
|
+
if DaemonController.can_ping_unix_sockets?
|
409
|
+
specify "the ping command may be [:unix, filename]" do
|
410
|
+
new_controller(:ping_command => [:unix, "spec/foo.sock"])
|
411
|
+
@controller.send(:run_ping_command).should be_false
|
380
412
|
|
381
|
-
|
382
|
-
begin
|
413
|
+
@server = UNIXServer.new('spec/foo.sock')
|
383
414
|
@controller.send(:run_ping_command).should be_true
|
384
|
-
|
385
|
-
|
415
|
+
end
|
416
|
+
else
|
417
|
+
specify "a ping command of type [:unix, filename] is not supported on this Ruby implementation" do
|
418
|
+
new_controller(:ping_command => [:unix, "spec/foo.sock"])
|
419
|
+
@server = UNIXServer.new('spec/foo.sock')
|
420
|
+
lambda { @controller.send(:run_ping_command) }.should raise_error(
|
421
|
+
"Pinging Unix domain sockets is not supported on this Ruby implementation")
|
386
422
|
end
|
387
423
|
end
|
388
424
|
end
|
data/spec/echo_server.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
#!/usr/bin/
|
1
|
+
#!/usr/bin/ruby
|
2
2
|
# A simple echo server, used by the unit test.
|
3
|
+
# The hashbang is explicitly set to /usr/bin/ruby because we need
|
4
|
+
# a Ruby implementation that starts fast and supports forking. The
|
5
|
+
# Ruby in $PATH may be JRuby which is neither.
|
3
6
|
require 'socket'
|
4
7
|
require 'optparse'
|
5
8
|
|
@@ -61,6 +64,10 @@ if options[:pid_file]
|
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
67
|
+
if ENV['ENV_FILE']
|
68
|
+
options[:env_file] = File.expand_path(ENV['ENV_FILE'])
|
69
|
+
end
|
70
|
+
|
64
71
|
def main(options)
|
65
72
|
STDIN.reopen("/dev/null", 'r')
|
66
73
|
STDOUT.reopen(options[:log_file], 'a')
|
@@ -69,6 +76,15 @@ def main(options)
|
|
69
76
|
STDERR.sync = true
|
70
77
|
Dir.chdir(options[:chdir])
|
71
78
|
File.umask(0)
|
79
|
+
|
80
|
+
if options[:env_file]
|
81
|
+
File.open(options[:env_file], 'w') do |f|
|
82
|
+
f.write("\0")
|
83
|
+
end
|
84
|
+
at_exit do
|
85
|
+
File.unlink(options[:env_file]) rescue nil
|
86
|
+
end
|
87
|
+
end
|
72
88
|
|
73
89
|
if options[:pid_file]
|
74
90
|
sleep(options[:wait1])
|
@@ -122,4 +138,4 @@ if options[:daemonize]
|
|
122
138
|
end
|
123
139
|
else
|
124
140
|
main(options)
|
125
|
-
end
|
141
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: daemon_controller
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.0
|
10
|
+
version: 1.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Hongli Lai
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-
|
18
|
+
date: 2012-10-27 00:00:00 Z
|
19
19
|
dependencies: []
|
20
20
|
|
21
21
|
description: A library for robust daemon management.
|