servolux 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,8 +1,21 @@
1
+ == 0.4.0 / 2009-07-01
2
+
3
+ * 1 Minor Enhancement
4
+ * Added a "Child" class for working with child processes
5
+ * 1 Bug Fix
6
+ * Thread#join has a small bug in JRuby - implemented workaround
7
+
8
+ == 0.3.0 / 2009-06-24
9
+
10
+ * 2 Minor Enhancements
11
+ * Documentation
12
+ * Unit tests
13
+
1
14
  == 0.2.0 / 2009-06-19
2
15
 
3
16
  * 1 Minor Enhancement
4
17
  * Added a signal method to the Piper class for signaling the child
5
- * 1 bug fix
18
+ * 1 Bug Fix
6
19
  * Fixed a race condition in the Threaded#stop method
7
20
 
8
21
  == 0.1.0 / 2009-06-18
data/README.rdoc CHANGED
@@ -1,22 +1,17 @@
1
1
  = Serv-O-Lux
2
2
  * by Tim Pease
3
- * http://codeforpeople.rubyforge.org/servolux
3
+ * http://github.com/TwP/servolux/tree/master
4
4
 
5
5
  === DESCRIPTION:
6
6
 
7
- Threads : Servers : Forks : Daemons
7
+ Threads : Servers : Forks : Daemons : Serv-O-Lux has them all!
8
8
 
9
- === FEATURES/PROBLEMS:
9
+ === FEATURES:
10
10
 
11
- * FIXME (list of features or problems)
11
+ http://codeforpeople.rubyforge.org/servolux
12
12
 
13
13
  === SYNOPSIS:
14
14
 
15
- FIXME (code sample of usage)
16
-
17
- === REQUIREMENTS:
18
-
19
- * FIXME (list of requirements)
20
15
 
21
16
  === INSTALL:
22
17
 
data/lib/servolux.rb CHANGED
@@ -4,7 +4,7 @@ require 'logging'
4
4
  module Servolux
5
5
 
6
6
  # :stopdoc:
7
- VERSION = '0.2.0'
7
+ VERSION = '0.4.0'
8
8
  LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
9
9
  PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
10
10
  # :startdoc:
@@ -42,7 +42,7 @@ module Servolux
42
42
 
43
43
  end # module Servolux
44
44
 
45
- %w[threaded server piper daemon].each do |lib|
45
+ %w[threaded server piper daemon child].each do |lib|
46
46
  require Servolux.libpath('servolux', lib)
47
47
  end
48
48
 
@@ -0,0 +1,116 @@
1
+
2
+ #
3
+ #
4
+ class Servolux::Child
5
+
6
+ attr_accessor :command
7
+ attr_accessor :timeout
8
+ attr_accessor :signals
9
+ attr_accessor :suspend
10
+ attr_reader :io
11
+ attr_reader :pid
12
+
13
+ #
14
+ #
15
+ def initialize( command, opts = {} )
16
+ @command = command
17
+ @timeout = opts.getopt :timeout
18
+ @signals = opts.getopt :signals, %w[TERM QUIT KILL]
19
+ @suspend = opts.getopt :suspend, 4
20
+ @io = @pid = @thread = @timed_out = nil
21
+ end
22
+
23
+ #
24
+ #
25
+ def start( mode = 'r', &block )
26
+ start_timeout_thread if @timeout
27
+
28
+ @io = IO::popen @command, mode
29
+ @pid = @io.pid
30
+
31
+ return block.call(@io) unless block.nil?
32
+ self
33
+ end
34
+
35
+ #
36
+ #
37
+ def stop
38
+ unless @thread.nil?
39
+ t, @thread = @thread, nil
40
+ t[:stop] = true
41
+ t.wakeup.join if t.status
42
+ end
43
+
44
+ kill if alive?
45
+ @io.close rescue nil
46
+ @io = @pid = nil
47
+ self
48
+ end
49
+
50
+ # Waits for the child process to exit and returns its exit status. The
51
+ # global variable $? is set to a Process::Status object containing
52
+ # information on the child process.
53
+ #
54
+ def wait( flags = 0 )
55
+ return if @io.nil?
56
+ Process.wait(@pid, flags)
57
+ $?.exitstatus
58
+ end
59
+
60
+ # Returns +true+ if the child process is alive.
61
+ #
62
+ def alive?
63
+ return if @io.nil?
64
+ Process.kill(0, @pid)
65
+ true
66
+ rescue Errno::ESRCH, Errno::ENOENT
67
+ false
68
+ end
69
+
70
+ # Returns +true+ if the child process was killed by the timeout thread.
71
+ #
72
+ def timed_out?
73
+ @timed_out
74
+ end
75
+
76
+
77
+ private
78
+
79
+ #
80
+ #
81
+ def kill
82
+ return if @io.nil?
83
+
84
+ existed = false
85
+ @signals.each do |sig|
86
+ begin
87
+ Process.kill sig, @pid
88
+ existed = true
89
+ rescue Errno::ESRCH, Errno::ENOENT
90
+ return(existed ? nil : true)
91
+ end
92
+ return true unless alive?
93
+ sleep @suspend
94
+ return true unless alive?
95
+ end
96
+ return !alive?
97
+ end
98
+
99
+ #
100
+ #
101
+ def start_timeout_thread
102
+ @timed_out = false
103
+ @thread = Thread.new(self) { |child|
104
+ sleep @timeout
105
+ unless Thread.current[:stop]
106
+ if child.alive?
107
+ child.instance_variable_set(:@timed_out, true)
108
+ child.__send__(:kill)
109
+ end
110
+ end
111
+ }
112
+ end
113
+
114
+ end # class Servolux::Child
115
+
116
+ # EOF
@@ -1,13 +1,77 @@
1
-
2
1
  # == Synopsis
3
2
  # The Daemon takes care of the work of creating and managing daemon
4
- # processes from with Ruby.
3
+ # processes from Ruby.
4
+ #
5
+ # == Details
6
+ # A daemon process is a long running process on a UNIX system that is
7
+ # detached from a TTY -- i.e. it is not tied to a user session. These types
8
+ # of processes are notoriously difficult to setup correctly. This Daemon
9
+ # class encapsulates some best practices to ensure daemons startup properly
10
+ # and can be shutdown gracefully.
11
+ #
12
+ # Starting a daemon process involves forking a child process, setting the
13
+ # child as a session leader, forking again, and detaching from the current
14
+ # working directory and standard in/out/error file descriptors. Because of
15
+ # this separation between the parent process and the daemon process, it is
16
+ # difficult to know if the daemon started properly.
17
+ #
18
+ # The Daemon class opens a pipe between the parent and the daemon. The PID
19
+ # of the daemon is sent to the parent through this pipe. The PID is used to
20
+ # check if the daemon is alive. Along with the PID, any errors from the
21
+ # daemon process are marshalled through the pipe back to the parent. These
22
+ # errors are wrapped in a StartupError and then raised in the parent.
23
+ #
24
+ # If no errors are passed up the pipe, the parent process waits till the
25
+ # daemon starts. This is determined by sending a signal to the daemon
26
+ # process.
27
+ #
28
+ # If a log file is given to the Daemon instance, then it is monitored for a
29
+ # change in size and mtime. This lets the Daemon instance know that the
30
+ # daemon process is updating the log file. Furthermore, the log file can be
31
+ # watched for a specific pattern; this pattern signals that the daemon
32
+ # process is up and running.
33
+ #
34
+ # Shutting down the daemon process is a little simpler. An external shutdown
35
+ # command can be used, or the Daemon instance will send an INT or TERM
36
+ # signal to the daemon process.
37
+ #
38
+ # Again, the Daemon instance will wait till the daemon process shuts down.
39
+ # This is determined by attempting to signal the daemon process PID and then
40
+ # returning when this signal fails -- i.e. then the deamon process has died.
41
+ #
42
+ # == Examples
43
+ #
44
+ # ==== Bad Example
45
+ # This is a bad example. The daemon will not start because the startup
46
+ # command "/usr/bin/no-command-by-this-name" cannot be found on the file
47
+ # system. The daemon process will send an Errno::ENOENT through the pipe
48
+ # back to the parent which gets wrapped in a StartupError
49
+ #
50
+ # daemon = Servolux::Daemon.new(
51
+ # :name => 'Bad Example',
52
+ # :pid_file => '/dev/null',
53
+ # :startup_command => '/usr/bin/no-command-by-this-name'
54
+ # )
55
+ # daemon.startup #=> raises StartupError
56
+ #
57
+ # ==== Good Example
58
+ # This is a simple Ruby server that prints the time to a file every minute.
59
+ # So, it's not really a "good" example, but it will work.
60
+ #
61
+ # server = Servolux::Server.new('TimeStamp', :interval => 60)
62
+ # class << server
63
+ # def file() @fd ||= File.open('timestamps.txt', 'w'); end
64
+ # def run() file.puts Time.now; end
65
+ # end
66
+ #
67
+ # daemon = Servolux::Daemon.new(:server => server, :log_file => 'timestamps.txt')
68
+ # daemon.startup
5
69
  #
6
70
  class Servolux::Daemon
7
71
 
8
- Error = Class.new(StandardError)
9
- Timeout = Class.new(Error)
10
- AlreadyStarted = Class.new(Error)
72
+ Error = Class.new(::Servolux::Error)
73
+ Timeout = Class.new(Error)
74
+ StartupError = Class.new(Error)
11
75
 
12
76
  attr_reader :name
13
77
  attr_writer :logger
@@ -63,7 +127,9 @@ class Servolux::Daemon
63
127
  # prevents zombie processes. The default is false.
64
128
  #
65
129
  # * shutdown_command
66
- # TODO: Not Yet Implemented
130
+ # Assign the startup command. This can be either a String, an Array of
131
+ # strings, a Proc, a bound Method, or a Servolux::Server instance.
132
+ # Different calling semantics are used for each type of command.
67
133
  #
68
134
  # * log_file <String>
69
135
  # This log file will be monitored to determine if the daemon process
@@ -155,14 +221,38 @@ class Servolux::Daemon
155
221
  #
156
222
  def startup
157
223
  raise Error, "Fork is not supported in this Ruby environment." unless ::Servolux.fork?
224
+ return if alive?
225
+
226
+ logger.debug "About to fork ..."
227
+ @piper = ::Servolux::Piper.daemon(nochdir, noclose)
158
228
 
159
- if alive?
160
- raise AlreadyStarted,
161
- "#{name.inspect} is already running: " \
162
- "PID is #{retrieve_pid} from PID file #{pid_file.inspect}"
229
+ @piper.parent {
230
+ @piper.timeout = 0
231
+ wait_for_startup
232
+ exit!(0)
233
+ }
234
+
235
+ @piper.child { run_startup_command }
236
+ end
237
+
238
+ # Stop the daemon process. If a shutdown command has been defined, it will
239
+ # be called to stop the daemon process. Otherwise, SIGINT will be sent to
240
+ # the daemon process to terminate it.
241
+ #
242
+ def shutdown
243
+ return unless alive?
244
+
245
+ case shutdown_command
246
+ when nil; kill
247
+ when String; exec(shutdown_command)
248
+ when Array; exec(*shutdown_command)
249
+ when Proc, Method; shutdown_command.call
250
+ when ::Servolux::Server; shutdown_command.shutdown
251
+ else
252
+ raise Error, "Unrecognized shutdown command #{shutdown_command.inspect}"
163
253
  end
164
254
 
165
- daemonize
255
+ wait_for_shutdown
166
256
  end
167
257
 
168
258
  # Returns +true+ if the daemon processis currently running. Returns
@@ -190,7 +280,6 @@ class Servolux::Daemon
190
280
  pid = retrieve_pid
191
281
  logger.info "Killing PID #{pid} with #{signal}"
192
282
  Process.kill(signal, pid)
193
- wait_for_shutdown
194
283
  rescue Errno::EINVAL
195
284
  logger.error "Failed to kill PID #{pid} with #{signal}: " \
196
285
  "'#{signal}' is an invalid or unsupported signal number."
@@ -220,19 +309,6 @@ class Servolux::Daemon
220
309
 
221
310
  private
222
311
 
223
- def daemonize
224
- logger.debug "About to fork ..."
225
-
226
- @piper = ::Servolux::Piper.daemon(nochdir, noclose)
227
-
228
- @piper.parent {
229
- wait_for_startup
230
- exit!(0)
231
- }
232
-
233
- @piper.child { run_startup_command }
234
- end
235
-
236
312
  def run_startup_command
237
313
  case startup_command
238
314
  when String; exec(startup_command)
@@ -245,7 +321,7 @@ class Servolux::Daemon
245
321
 
246
322
  rescue Exception => err
247
323
  unless err.is_a?(SystemExit)
248
- logger.fatal err
324
+ logger.fatal err
249
325
  @piper.puts err
250
326
  end
251
327
  ensure
@@ -254,9 +330,10 @@ class Servolux::Daemon
254
330
 
255
331
  def exec( *args )
256
332
  logger.debug "Calling: exec(*#{args.inspect})"
257
- std = [STDIN, STDOUT, STDERR, @piper.write_io]
333
+ skip = [STDIN, STDOUT, STDERR]
334
+ skip << @piper.write_io if @piper
258
335
  ObjectSpace.each_object(IO) { |obj|
259
- next if std.include? obj
336
+ next if skip.include? obj
260
337
  obj.close rescue nil
261
338
  }
262
339
  Kernel.exec(*args)
@@ -279,56 +356,19 @@ class Servolux::Daemon
279
356
  def wait_for_startup
280
357
  logger.debug "Waiting for #{name.inspect} to startup."
281
358
 
282
- begin
283
- started = wait_for {
284
- rv = started?
285
- err = @piper.gets; raise err unless err.nil?
286
- rv
287
- }
288
-
289
- if started
290
- logger.info 'Server has daemonized.'
291
- return
292
- end
293
- rescue Exception => err
294
- child_err = @piper.gets
295
- raise child_err || err
296
- ensure
297
- @piper.close
298
- end
299
-
300
- # if the daemon doesn't fork into the background in time, then kill it.
301
- force_kill
302
- end
303
-
304
- def force_kill
305
- pid = retrieve_pid
306
-
307
- t = Thread.new {
308
- begin
309
- sleep 7
310
- unless Thread.current[:stop]
311
- Process.kill('KILL', pid)
312
- Process.waitpid(pid)
313
- end
314
- rescue Exception
315
- end
359
+ started = wait_for {
360
+ rv = started?
361
+ err = @piper.gets
362
+ raise StartupError, "Child raised error: #{err.inspect}" unless err.nil?
363
+ rv
316
364
  }
317
365
 
318
- Process.kill('TERM', pid) rescue nil
319
- Process.waitpid(pid) rescue nil
320
- t[:stop] = true
321
- t.run if t.status
322
- t.join
323
-
324
366
  raise Timeout, "#{name.inspect} failed to startup in a timely fashion. " \
325
- "The timeout is set at #{timeout} seconds."
367
+ "The timeout is set at #{timeout} seconds." unless started
326
368
 
327
- rescue Errno::ENOENT
328
- raise Timeout, "Could not find a PID file at #{pid_file.inspect}."
329
- rescue Errno::EACCES => err
330
- raise Timeout, "You do not have access to the PID file at " \
331
- "#{pid_file.inspect}: #{err.message}"
369
+ logger.info 'Server has daemonized.'
370
+ ensure
371
+ @piper.close
332
372
  end
333
373
 
334
374
  def wait_for_shutdown
@@ -340,14 +380,14 @@ class Servolux::Daemon
340
380
 
341
381
  def wait_for
342
382
  start = Time.now
343
- nap_time = 0.1
383
+ nap_time = 0.2
344
384
 
345
385
  loop do
346
386
  sleep nap_time
347
387
 
348
388
  diff = Time.now - start
349
389
  nap_time = 2*nap_time
350
- nap_time = diff + 0.1 if diff < nap_time
390
+ nap_time = 0.2 if nap_time > 1.6
351
391
 
352
392
  break true if yield
353
393
  break false if diff >= timeout
@@ -66,6 +66,7 @@ class Servolux::Piper
66
66
  piper = self.new(:timeout => 1)
67
67
  piper.parent {
68
68
  pid = piper.gets
69
+ raise ::Servolux::Error, 'Could not get the child PID.' if pid.nil?
69
70
  piper.instance_variable_set(:@child_pid, pid)
70
71
  }
71
72
  piper.child {
@@ -83,7 +84,7 @@ class Servolux::Piper
83
84
 
84
85
  piper.puts Process.pid
85
86
  }
86
- piper
87
+ return piper
87
88
  end
88
89
 
89
90
  # The timeout in seconds to wait for puts / gets commands.
@@ -121,7 +122,11 @@ class Servolux::Piper
121
122
  end
122
123
 
123
124
  @timeout = opts.getopt(:timeout, 0)
124
- @read_io, @write_io = IO.pipe
125
+ if defined? ::Encoding
126
+ @read_io, @write_io = IO.pipe('ASCII-8BIT') # encoding for Ruby 1.9
127
+ else
128
+ @read_io, @write_io = IO.pipe
129
+ end
125
130
  @child_pid = Kernel.fork
126
131
 
127
132
  if child?
@@ -99,6 +99,7 @@
99
99
  #
100
100
  # module DebugSignal
101
101
  # def usr1
102
+ # @old_log_level ||= nil
102
103
  # if @old_log_level
103
104
  # logger.level = @old_log_level
104
105
  # @old_log_level = nil
@@ -127,7 +128,7 @@ class Servolux::Server
127
128
  Error = Class.new(::Servolux::Error)
128
129
 
129
130
  attr_reader :name
130
- attr_writer :logger
131
+ attr_accessor :logger
131
132
  attr_writer :pid_file
132
133
 
133
134
  # call-seq:
@@ -144,6 +145,8 @@ class Servolux::Server
144
145
  #
145
146
  def initialize( name, opts = {}, &block )
146
147
  @name = name
148
+ @activity_thread = nil
149
+ @activity_thread_running = false
147
150
 
148
151
  self.logger = opts.getopt :logger
149
152
  self.pid_file = opts.getopt :pid_file
@@ -185,14 +188,6 @@ class Servolux::Server
185
188
  alias :term :stop # handles the TERM signal
186
189
  private :start, :stop
187
190
 
188
- # Returns the logger instance used by the server. If none was given, then
189
- # a logger is created from the Logging framework (see the Logging rubygem
190
- # for more information).
191
- #
192
- def logger
193
- @logger ||= Logging.logger[self]
194
- end
195
-
196
191
  # Returns the PID file name used by the server. If none was given, then
197
192
  # the server name is used to create a PID file name.
198
193
  #
@@ -40,7 +40,7 @@ module Servolux::Threaded
40
40
  #
41
41
  def run
42
42
  raise NotImplementedError,
43
- 'This method must be defined by the threaded object.'
43
+ 'The run method must be defined by the threaded object.'
44
44
  end
45
45
 
46
46
  # Start the activity thread. If already started this method will return
@@ -64,30 +64,33 @@ module Servolux::Threaded
64
64
  break unless running?
65
65
  run
66
66
  }
67
- rescue Exception => e
68
- logger.fatal e
67
+ rescue Exception => err
68
+ @activity_thread_running = false
69
+ logger.fatal err unless err.is_a?(SystemExit)
70
+ raise err
69
71
  end
70
72
  }
71
73
  after_starting if self.respond_to?(:after_starting)
72
74
  self
73
75
  end
74
76
 
75
- # Stop the activity thread. If already stopped this method will return
76
- # without taking any action.
77
+ # Stop the activity thread. If already stopped this method will return
78
+ # without taking any action. Otherwise, this method does not return until
79
+ # the activity thread has died or until _limit_ seconds have passed.
77
80
  #
78
81
  # If the including class defines a 'before_stopping' method, it will be
79
82
  # called before the thread is stopped. Likewise, if the including class
80
83
  # defines an 'after_stopping' method, it will be called after the thread
81
84
  # has stopped.
82
85
  #
83
- def stop
86
+ def stop( limit = nil )
84
87
  return self unless running?
85
88
  logger.debug "Stopping"
86
89
 
87
90
  @activity_thread_running = false
88
91
  before_stopping if self.respond_to?(:before_stopping)
89
92
  @activity_thread.wakeup
90
- @activity_thread.join
93
+ join limit
91
94
  @activity_thread = nil
92
95
  after_stopping if self.respond_to?(:after_stopping)
93
96
  self
@@ -101,10 +104,8 @@ module Servolux::Threaded
101
104
  # with +nil+.
102
105
  #
103
106
  def join( limit = nil )
104
- @activity_thread.join limit
105
- self
106
- rescue NoMethodError
107
- return self
107
+ return if @activity_thread.nil?
108
+ @activity_thread.join(limit) ? self : nil
108
109
  end
109
110
 
110
111
  # Returns +true+ if the activity thread is running. Returns +false+
@@ -114,6 +115,22 @@ module Servolux::Threaded
114
115
  @activity_thread_running
115
116
  end
116
117
 
118
+ # Returns the status of threaded object.
119
+ #
120
+ # 'sleep' : sleeping or waiting on I/O
121
+ # 'run' : executing
122
+ # 'aborting' : aborting
123
+ # false : not running or terminated normally
124
+ # nil : terminated with an exception
125
+ #
126
+ # If this method returns +nil+, then calling join on the threaded object
127
+ # will cause the exception to be raised in the calling thread.
128
+ #
129
+ def status
130
+ return false if @activity_thread.nil?
131
+ @activity_thread.status
132
+ end
133
+
117
134
  # Sets the number of seconds to sleep between invocations of the
118
135
  # threaded object's 'run' method.
119
136
  #
@@ -128,6 +145,26 @@ module Servolux::Threaded
128
145
  @activity_thread_interval
129
146
  end
130
147
 
148
+ # :stopdoc:
149
+ #
150
+ # The JRuby platform has an implementation error in it's Thread#join
151
+ # method. In the Matz Ruby Interpreter, Thread#join with a +nil+ argument
152
+ # will sleep forever; in the JRuby implementation, join will return
153
+ # immediately.
154
+ #
155
+ if 'java' == RUBY_PLATFORM
156
+ undef :join
157
+ def join( limit = nil )
158
+ return if @activity_thread.nil?
159
+ if limit.nil?
160
+ @activity_thread.join ? self : nil
161
+ else
162
+ @activity_thread.join(limit) ? self : nil
163
+ end
164
+ end
165
+ end
166
+ # :startdoc:
167
+
131
168
  end # module Servolux::Threaded
132
169
 
133
170
  # EOF
@@ -0,0 +1,110 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ if Servolux.fork?
5
+
6
+ describe Servolux::Piper do
7
+
8
+ before :each do
9
+ @piper = nil
10
+ end
11
+
12
+ after :each do
13
+ next if @piper.nil?
14
+ @piper.puts :die
15
+ @piper.close
16
+ @piper = nil
17
+ end
18
+
19
+ it 'only understands three file modes' do
20
+ %w[r w rw].each do |mode|
21
+ lambda {
22
+ piper = Servolux::Piper.new(mode)
23
+ piper.child { piper.close; exit! }
24
+ piper.parent { piper.close }
25
+ }.should_not raise_error
26
+ end
27
+
28
+ lambda { Servolux::Piper.new('f') }.should raise_error(
29
+ ArgumentError, 'Unsupported mode "f"')
30
+ end
31
+
32
+ it 'enables communication between parents and children' do
33
+ @piper = Servolux::Piper.new 'rw', :timeout => 2
34
+
35
+ @piper.child {
36
+ loop {
37
+ obj = @piper.gets
38
+ if :die == obj
39
+ @piper.close; exit!
40
+ end
41
+ @piper.puts obj unless obj.nil?
42
+ }
43
+ }
44
+
45
+ @piper.parent {
46
+ @piper.puts 'foo bar baz'
47
+ @piper.gets.should == 'foo bar baz'
48
+
49
+ @piper.puts %w[one two three]
50
+ @piper.gets.should == %w[one two three]
51
+
52
+ @piper.puts('Returns # of bytes written').should > 0
53
+ @piper.gets.should == 'Returns # of bytes written'
54
+
55
+ @piper.puts 1
56
+ @piper.puts 2
57
+ @piper.puts 3
58
+ @piper.gets.should == 1
59
+ @piper.gets.should == 2
60
+ @piper.gets.should == 3
61
+
62
+ @piper.timeout = 0
63
+ @piper.readable?.should be_false
64
+ }
65
+ end
66
+
67
+ it 'sends signals from parent to child' do
68
+ @piper = Servolux::Piper.new :timeout => 2
69
+
70
+ @piper.child {
71
+ Signal.trap('USR2') { @piper.puts "'USR2' was received" rescue nil }
72
+ Signal.trap('INT') {
73
+ @piper.puts "'INT' was received" rescue nil
74
+ @piper.close
75
+ exit!
76
+ }
77
+ Thread.new { sleep 7; exit! }
78
+ @piper.puts :ready
79
+ loop { sleep }
80
+ }
81
+
82
+ @piper.parent {
83
+ @piper.gets.should == :ready
84
+
85
+ @piper.signal 'USR2'
86
+ @piper.gets.should == "'USR2' was received"
87
+
88
+ @piper.signal 'INT'
89
+ @piper.gets.should == "'INT' was received"
90
+ }
91
+ end
92
+
93
+ it 'creates a daemon process' do
94
+ @piper = Servolux::Piper.daemon(true, true)
95
+
96
+ @piper.child {
97
+ @piper.puts Process.ppid
98
+ @piper.close
99
+ exit!
100
+ }
101
+
102
+ @piper.parent {
103
+ @piper.gets.should == 1
104
+ }
105
+ end
106
+
107
+ end
108
+ end # if Servolux.fork?
109
+
110
+ # EOF
@@ -0,0 +1,69 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Servolux::Server do
5
+ base = Class.new(Servolux::Server) do
6
+ def initialize( &block )
7
+ super('Test Server', :logger => Logging.logger['Servolux'], &block)
8
+ end
9
+ def run() sleep; end
10
+ end
11
+
12
+ before :each do
13
+ @server = base.new
14
+ File.delete @server.pid_file if test(?f, @server.pid_file)
15
+ end
16
+
17
+ after :each do
18
+ File.delete @server.pid_file if test(?f, @server.pid_file)
19
+ end
20
+
21
+ it 'generates a PID file' do
22
+ test(?e, @server.pid_file).should be_false
23
+
24
+ t = Thread.new {@server.startup}
25
+ Thread.pass until @server.status == 'sleep'
26
+ test(?e, @server.pid_file).should be_true
27
+
28
+ @server.shutdown
29
+ Thread.pass until t.status == false
30
+ test(?e, @server.pid_file).should be_false
31
+ end
32
+
33
+ it 'shuts down gracefully when signaled' do
34
+ t = Thread.new {@server.startup}
35
+ Thread.pass until @server.status == 'sleep'
36
+ @server.running?.should be_true
37
+
38
+ Process.kill('INT', $$)
39
+ Thread.pass until t.status == false
40
+ @server.running?.should be_false
41
+ end
42
+
43
+ it 'responds to signals that have defined handlers' do
44
+ class << @server
45
+ def hup() logger.info 'hup was called'; end
46
+ def usr1() logger.info 'usr1 was called'; end
47
+ def usr2() logger.info 'usr2 was called'; end
48
+ end
49
+
50
+ t = Thread.new {@server.startup}
51
+ Thread.pass until @server.status == 'sleep'
52
+ @log_output.readline
53
+
54
+ Process.kill('USR1', $$)
55
+ @log_output.readline.strip.should == 'INFO Servolux : usr1 was called'
56
+
57
+ Process.kill('HUP', $$)
58
+ @log_output.readline.strip.should == 'INFO Servolux : hup was called'
59
+
60
+ Process.kill('USR2', $$)
61
+ @log_output.readline.strip.should == 'INFO Servolux : usr2 was called'
62
+
63
+ Process.kill('TERM', $$)
64
+ Thread.pass until t.status == false
65
+ @server.running?.should be_false
66
+ end
67
+ end
68
+
69
+ # EOF
data/spec/spec_helper.rb CHANGED
@@ -9,6 +9,8 @@ require 'spec/logging_helper'
9
9
  require File.expand_path(
10
10
  File.join(File.dirname(__FILE__), %w[.. lib servolux]))
11
11
 
12
+ include Logging.globally
13
+
12
14
  Spec::Runner.configure do |config|
13
15
  include Spec::LoggingHelper
14
16
  config.capture_log_messages
@@ -0,0 +1,105 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Servolux::Threaded do
5
+
6
+ base = Class.new do
7
+ include Servolux::Threaded
8
+ def initialize
9
+ @activity_thread_running = false
10
+ @activity_thread_interval = 0
11
+ end
12
+ def pass( val = 'sleep' )
13
+ Thread.pass until status == val
14
+ end
15
+ end
16
+
17
+ it "let's you know that it is running" do
18
+ klass = Class.new(base) do
19
+ def run() sleep 1; end
20
+ end
21
+
22
+ obj = klass.new
23
+ obj.interval = 0
24
+ obj.running?.should be_false
25
+
26
+ obj.start
27
+ obj.running?.should be_true
28
+ obj.pass
29
+
30
+ obj.stop(2)
31
+ obj.running?.should be_false
32
+ end
33
+
34
+ it "stops even when sleeping in the run method" do
35
+ klass = Class.new(base) do
36
+ attr_reader :stopped
37
+ def run() sleep; end
38
+ def after_starting() @stopped = false; end
39
+ def after_stopping() @stopped = true; end
40
+ end
41
+
42
+ obj = klass.new
43
+ obj.interval = 0
44
+ obj.stopped.should be_nil
45
+
46
+ obj.start
47
+ obj.stopped.should be_false
48
+ obj.pass
49
+
50
+ obj.stop(2)
51
+ obj.stopped.should be_true
52
+ end
53
+
54
+ it "calls all the before and after hooks" do
55
+ klass = Class.new(base) do
56
+ attr_accessor :ary
57
+ def run() sleep 1; end
58
+ def before_starting() ary << 1; end
59
+ def after_starting() ary << 2; end
60
+ def before_stopping() ary << 3; end
61
+ def after_stopping() ary << 4; end
62
+ end
63
+
64
+ obj = klass.new
65
+ obj.interval = 86400
66
+ obj.ary = []
67
+
68
+ obj.start
69
+ obj.ary.should == [1,2]
70
+ obj.pass
71
+
72
+ obj.stop(2)
73
+ obj.ary.should == [1,2,3,4]
74
+ end
75
+
76
+ it "dies when an exception is thrown" do
77
+ klass = Class.new(base) do
78
+ def run() raise 'ni'; end
79
+ end
80
+
81
+ obj = klass.new
82
+
83
+ obj.start
84
+ obj.pass nil
85
+
86
+ obj.running?.should be_false
87
+ @log_output.readline
88
+ @log_output.readline.chomp.should == "FATAL Object : <RuntimeError> ni"
89
+
90
+ lambda { obj.join }.should raise_error(RuntimeError, 'ni')
91
+ end
92
+
93
+ it "complains loudly if you don't have a run method" do
94
+ obj = base.new
95
+ obj.start
96
+ obj.pass nil
97
+
98
+ @log_output.readline
99
+ @log_output.readline.chomp.should == "FATAL Object : <NotImplementedError> The run method must be defined by the threaded object."
100
+
101
+ lambda { obj.join }.should raise_error(NotImplementedError, 'The run method must be defined by the threaded object.')
102
+ end
103
+ end
104
+
105
+ # EOF
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servolux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pease
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-19 00:00:00 -06:00
12
+ date: 2009-06-29 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -42,7 +42,7 @@ dependencies:
42
42
  - !ruby/object:Gem::Version
43
43
  version: 2.5.0
44
44
  version:
45
- description: "Threads : Servers : Forks : Daemons"
45
+ description: "Threads : Servers : Forks : Daemons : Serv-O-Lux has them all!"
46
46
  email: tim.pease@gmail.com
47
47
  executables: []
48
48
 
@@ -56,12 +56,16 @@ files:
56
56
  - README.rdoc
57
57
  - Rakefile
58
58
  - lib/servolux.rb
59
+ - lib/servolux/child.rb
59
60
  - lib/servolux/daemon.rb
60
61
  - lib/servolux/piper.rb
61
62
  - lib/servolux/server.rb
62
63
  - lib/servolux/threaded.rb
64
+ - spec/piper_spec.rb
65
+ - spec/server_spec.rb
63
66
  - spec/servolux_spec.rb
64
67
  - spec/spec_helper.rb
68
+ - spec/threaded_spec.rb
65
69
  - tasks/ann.rake
66
70
  - tasks/bones.rake
67
71
  - tasks/gem.rake
@@ -101,6 +105,6 @@ rubyforge_project: codeforpeople
101
105
  rubygems_version: 1.3.1
102
106
  signing_key:
103
107
  specification_version: 2
104
- summary: "Threads : Servers : Forks : Daemons"
108
+ summary: "Threads : Servers : Forks : Daemons : Serv-O-Lux has them all!"
105
109
  test_files: []
106
110