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 +14 -1
- data/README.rdoc +4 -9
- data/lib/servolux.rb +2 -2
- data/lib/servolux/child.rb +116 -0
- data/lib/servolux/daemon.rb +116 -76
- data/lib/servolux/piper.rb +7 -2
- data/lib/servolux/server.rb +4 -9
- data/lib/servolux/threaded.rb +48 -11
- data/spec/piper_spec.rb +110 -0
- data/spec/server_spec.rb +69 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/threaded_spec.rb +105 -0
- metadata +8 -4
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
|
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://
|
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
|
9
|
+
=== FEATURES:
|
10
10
|
|
11
|
-
|
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.
|
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
|
data/lib/servolux/daemon.rb
CHANGED
@@ -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
|
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
|
9
|
-
Timeout
|
10
|
-
|
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
|
-
#
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
333
|
+
skip = [STDIN, STDOUT, STDERR]
|
334
|
+
skip << @piper.write_io if @piper
|
258
335
|
ObjectSpace.each_object(IO) { |obj|
|
259
|
-
next if
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
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.
|
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 =
|
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
|
data/lib/servolux/piper.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/servolux/server.rb
CHANGED
@@ -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
|
-
|
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
|
#
|
data/lib/servolux/threaded.rb
CHANGED
@@ -40,7 +40,7 @@ module Servolux::Threaded
|
|
40
40
|
#
|
41
41
|
def run
|
42
42
|
raise NotImplementedError,
|
43
|
-
'
|
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 =>
|
68
|
-
|
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
|
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
|
-
|
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.
|
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
|
data/spec/piper_spec.rb
ADDED
@@ -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
|
data/spec/server_spec.rb
ADDED
@@ -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
@@ -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.
|
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-
|
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
|
|