servolux 0.2.0 → 0.4.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/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