daemon_controller 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|