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 +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
|
|