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.
@@ -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.0.0"
5
- s.date = "2012-02-04"
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"
@@ -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? || Process.respond_to?(:spawn)
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
- ruby_interpreter = File.join(
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 += " 2>\"#{tempfile_path}\"" unless PLATFORM =~ /mswin/
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
- case type
670
- when :tcp
671
- socket_domain = Socket::Constants::AF_INET
672
- hostname, port = args
673
- sockaddr = Socket.pack_sockaddr_in(port, hostname)
674
- when :unix
675
- socket_domain = Socket::Constants::AF_LOCAL
676
- sockaddr = Socket.pack_sockaddr_un(args[0])
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
- raise ArgumentError, "Unknown ping command type #{type.inspect}"
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
- # Used on Ruby 1.9 because forking may not be safe/supported on all platforms.
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])
@@ -21,7 +21,7 @@
21
21
 
22
22
  class DaemonController
23
23
  MAJOR = 1
24
- MINOR = 0
24
+ MINOR = 1
25
25
  TINY = 0
26
26
  VERSION_STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
27
27
  end # class DaemonController
@@ -128,7 +128,7 @@ describe DaemonController, "#start" do
128
128
  end
129
129
  end
130
130
 
131
- if DaemonController.send(:fork_supported?) || Process.respond_to?(:spawn)
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?) || Process.respond_to?(:spawn)
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
- begin
337
- socket = nil
338
- new_controller(:ping_command => lambda do
339
- socket = TCPSocket.new('localhost', 8278)
340
- end)
341
- @controller.send(:run_ping_command)
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
- begin
352
- o = Object.new
353
- o.should_receive(:close).and_return do
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, "localhost", 8278])
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('localhost', 8278)
370
- begin
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
- specify "the ping command may be [:unix, filename]" do
378
- new_controller(:ping_command => [:unix, "spec/foo.sock"])
379
- @controller.send(:run_ping_command).should be_false
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
- server = UNIXServer.new('spec/foo.sock')
382
- begin
413
+ @server = UNIXServer.new('spec/foo.sock')
383
414
  @controller.send(:run_ping_command).should be_true
384
- ensure
385
- server.close
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
@@ -1,5 +1,8 @@
1
- #!/usr/bin/env ruby
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: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 0
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-02-04 00:00:00 Z
18
+ date: 2012-10-27 00:00:00 Z
19
19
  dependencies: []
20
20
 
21
21
  description: A library for robust daemon management.