servolux 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/README.rdoc +48 -0
- data/Rakefile +41 -0
- data/lib/servolux/daemon.rb +403 -0
- data/lib/servolux/piper.rb +274 -0
- data/lib/servolux/server.rb +226 -0
- data/lib/servolux/threaded.rb +133 -0
- data/lib/servolux.rb +49 -0
- data/spec/servolux_spec.rb +20 -0
- data/spec/spec_helper.rb +27 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- metadata +106 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
|
2
|
+
# == Synopsis
|
3
|
+
# A Piper is used to fork a child proces and then establish a communication
|
4
|
+
# pipe between the parent and child. This communication pipe is used to pass
|
5
|
+
# Ruby objects between the two.
|
6
|
+
#
|
7
|
+
# == Details
|
8
|
+
# When a new piper instance is created, the Ruby process is forked into two
|
9
|
+
# porcesses - the parent and the child. Each continues execution from the
|
10
|
+
# point of the fork. The piper establishes a pipe for communication between
|
11
|
+
# the parent and the child. This communication pipe can be opened as read /
|
12
|
+
# write / read-write (from the perspective of the parent).
|
13
|
+
#
|
14
|
+
# Communication over the pipe is handled by marshalling Ruby objects through
|
15
|
+
# the pipe. This means that nearly any Ruby object can be passed between the
|
16
|
+
# two processes. For example, exceptions from the child process can be
|
17
|
+
# marshalled back to the parent and raised there.
|
18
|
+
#
|
19
|
+
# Object passing is handled by use of the +puts+ and +gets+ methods defined
|
20
|
+
# on the Piper. These methods use a +timeout+ and the Kernel#select method
|
21
|
+
# to ensure a timely return.
|
22
|
+
#
|
23
|
+
# == Examples
|
24
|
+
#
|
25
|
+
# piper = Servolux::Piper.new('r', :timeout => 5)
|
26
|
+
#
|
27
|
+
# piper.parent {
|
28
|
+
# $stdout.puts "parent pid #{Process.pid}"
|
29
|
+
# $stdout.puts "child pid #{piper.pid} [from fork]"
|
30
|
+
#
|
31
|
+
# child_pid = piper.gets
|
32
|
+
# $stdout.puts "child pid #{child_pid} [from child]"
|
33
|
+
#
|
34
|
+
# msg = piper.gets
|
35
|
+
# $stdout.puts "message from child #{msg.inspect}"
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# piper.child {
|
39
|
+
# sleep 2
|
40
|
+
# piper.puts Process.pid
|
41
|
+
# sleep 3
|
42
|
+
# piper.puts "The time is #{Time.now}"
|
43
|
+
# }
|
44
|
+
#
|
45
|
+
# piper.close
|
46
|
+
#
|
47
|
+
class Servolux::Piper
|
48
|
+
|
49
|
+
# :stopdoc:
|
50
|
+
SEPERATOR = [0xDEAD, 0xBEEF].pack('n*').freeze
|
51
|
+
# :startdoc:
|
52
|
+
|
53
|
+
# call-seq:
|
54
|
+
# Piper.daemon( nochdir = false, noclose = false )
|
55
|
+
#
|
56
|
+
# Creates a new Piper with the child process configured as a daemon. The
|
57
|
+
# +pid+ method of the piper returns the PID of the daemon process.
|
58
|
+
#
|
59
|
+
# Be default a daemon process will release its current working directory
|
60
|
+
# and the stdout/stderr/stdin file descriptors. This allows the parent
|
61
|
+
# process to exit cleanly. This behavior can be overridden by setting the
|
62
|
+
# _nochdir_ and _noclose_ flags to true. The first will keep the current
|
63
|
+
# working directory; the second will keep stdout/stderr/stdin open.
|
64
|
+
#
|
65
|
+
def self.daemon( nochdir = false, noclose = false )
|
66
|
+
piper = self.new(:timeout => 1)
|
67
|
+
piper.parent {
|
68
|
+
pid = piper.gets
|
69
|
+
piper.instance_variable_set(:@child_pid, pid)
|
70
|
+
}
|
71
|
+
piper.child {
|
72
|
+
Process.setsid # Become session leader.
|
73
|
+
exit!(0) if fork # Zap session leader.
|
74
|
+
|
75
|
+
Dir.chdir '/' unless nochdir # Release old working directory.
|
76
|
+
File.umask 0000 # Ensure sensible umask.
|
77
|
+
|
78
|
+
unless noclose
|
79
|
+
STDIN.reopen '/dev/null' # Free file descriptors and
|
80
|
+
STDOUT.reopen '/dev/null', 'a' # point them somewhere sensible.
|
81
|
+
STDERR.reopen '/dev/null', 'a'
|
82
|
+
end
|
83
|
+
|
84
|
+
piper.puts Process.pid
|
85
|
+
}
|
86
|
+
piper
|
87
|
+
end
|
88
|
+
|
89
|
+
# The timeout in seconds to wait for puts / gets commands.
|
90
|
+
attr_accessor :timeout
|
91
|
+
|
92
|
+
# The read end of the pipe.
|
93
|
+
attr_reader :read_io
|
94
|
+
|
95
|
+
# The write end of the pipe.
|
96
|
+
attr_reader :write_io
|
97
|
+
|
98
|
+
# call-seq:
|
99
|
+
# Piper.new( mode = 'r', opts = {} )
|
100
|
+
#
|
101
|
+
# Creates a new Piper instance with the communication pipe configured
|
102
|
+
# using the provided _mode_. The default mode is read-only (from the
|
103
|
+
# parent, and write-only from the child). The supported modes are as
|
104
|
+
# follows:
|
105
|
+
#
|
106
|
+
# Mode | Parent View | Child View
|
107
|
+
# -----+-------------+-----------
|
108
|
+
# r read-only write-only
|
109
|
+
# w write-only read-only
|
110
|
+
# rw read-write read-write
|
111
|
+
#
|
112
|
+
# The communication timeout can be provided as an option. This is the
|
113
|
+
# number of seconds to wait for a +puts+ or +gets+ to succeed.
|
114
|
+
#
|
115
|
+
def initialize( *args )
|
116
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
117
|
+
mode = args.first || 'r'
|
118
|
+
|
119
|
+
unless %w[r w rw].include? mode
|
120
|
+
raise ArgumentError, "Unsupported mode #{mode.inspect}"
|
121
|
+
end
|
122
|
+
|
123
|
+
@timeout = opts.getopt(:timeout, 0)
|
124
|
+
@read_io, @write_io = IO.pipe
|
125
|
+
@child_pid = Kernel.fork
|
126
|
+
|
127
|
+
if child?
|
128
|
+
case mode
|
129
|
+
when 'r'; close_read
|
130
|
+
when 'w'; close_write
|
131
|
+
end
|
132
|
+
else
|
133
|
+
case mode
|
134
|
+
when 'r'; close_write
|
135
|
+
when 'w'; close_read
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Close both the read and write ends of the communications pipe. This only
|
141
|
+
# affects the process from which it was called -- the parent or the child.
|
142
|
+
#
|
143
|
+
def close
|
144
|
+
@read_io.close rescue nil
|
145
|
+
@write_io.close rescue nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# Close the read end of the communications pipe. This only affects the
|
149
|
+
# process from which it was called -- the parent or the child.
|
150
|
+
#
|
151
|
+
def close_read
|
152
|
+
@read_io.close rescue nil
|
153
|
+
end
|
154
|
+
|
155
|
+
# Close the write end of the communications pipe. This only affects the
|
156
|
+
# process from which it was called -- the parent or the child.
|
157
|
+
#
|
158
|
+
def close_write
|
159
|
+
@write_io.close rescue nil
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns +true+ if the communications pipe is readable from the process
|
163
|
+
# and there is data waiting to be read.
|
164
|
+
#
|
165
|
+
def readable?
|
166
|
+
return false if @read_io.closed?
|
167
|
+
r,w,e = Kernel.select([@read_io], nil, nil, @timeout)
|
168
|
+
return !(r.nil? or r.empty?)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns +true+ if the communications pipe is writeable from the process
|
172
|
+
# and the write buffer can accept more data.
|
173
|
+
#
|
174
|
+
def writeable?
|
175
|
+
return false if @write_io.closed?
|
176
|
+
r,w,e = Kernel.select(nil, [@write_io], nil, @timeout)
|
177
|
+
return !(w.nil? or w.empty?)
|
178
|
+
end
|
179
|
+
|
180
|
+
# call-seq:
|
181
|
+
# child { block }
|
182
|
+
# child {|piper| block }
|
183
|
+
#
|
184
|
+
# Execute the _block_ only in the child process. This method returns
|
185
|
+
# immediately when called from the parent process.
|
186
|
+
#
|
187
|
+
def child( &block )
|
188
|
+
return unless child?
|
189
|
+
raise ArgumentError, "A block must be supplied" if block.nil?
|
190
|
+
|
191
|
+
if block.arity > 0
|
192
|
+
block.call(self)
|
193
|
+
else
|
194
|
+
block.call
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns +true+ if this is the child prcoess and +false+ otherwise.
|
199
|
+
#
|
200
|
+
def child?
|
201
|
+
@child_pid.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
# call-seq:
|
205
|
+
# parent { block }
|
206
|
+
# parent {|piper| block }
|
207
|
+
#
|
208
|
+
# Execute the _block_ only in the parent process. This method returns
|
209
|
+
# immediately when called from the child process.
|
210
|
+
#
|
211
|
+
def parent( &block )
|
212
|
+
return unless parent?
|
213
|
+
raise ArgumentError, "A block must be supplied" if block.nil?
|
214
|
+
|
215
|
+
if block.arity > 0
|
216
|
+
block.call(self)
|
217
|
+
else
|
218
|
+
block.call
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Returns +true+ if this is the parent prcoess and +false+ otherwise.
|
223
|
+
#
|
224
|
+
def parent?
|
225
|
+
!@child_pid.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
# Returns the PID of the child process when called from the parent.
|
229
|
+
# Returns +nil+ when called from the child.
|
230
|
+
#
|
231
|
+
def pid
|
232
|
+
@child_pid
|
233
|
+
end
|
234
|
+
|
235
|
+
# Read an object from the communication pipe. Returns +nil+ if the pipe is
|
236
|
+
# closed for reading or if no data is available before the timeout
|
237
|
+
# expires. If data is available then it is un-marshalled and returned as a
|
238
|
+
# Ruby object.
|
239
|
+
#
|
240
|
+
# This method will block until the +timeout+ is reached or data can be
|
241
|
+
# read from the pipe.
|
242
|
+
#
|
243
|
+
def gets
|
244
|
+
return unless readable?
|
245
|
+
|
246
|
+
data = @read_io.gets SEPERATOR
|
247
|
+
return if data.nil?
|
248
|
+
|
249
|
+
data.chomp! SEPERATOR
|
250
|
+
Marshal.load(data) rescue data
|
251
|
+
end
|
252
|
+
|
253
|
+
# Write an object to the communication pipe. Returns +nil+ if the pipe is
|
254
|
+
# closed for writing or if the write buffer is full. The _obj_ is
|
255
|
+
# marshalled and written to the pipe (therefore, procs and other
|
256
|
+
# un-marshallable Ruby objects cannot be passed through the pipe).
|
257
|
+
#
|
258
|
+
# If the write is successful, then the number of bytes written to the pipe
|
259
|
+
# is returned. If this number is zero it means that the _obj_ was
|
260
|
+
# unsuccessfully communicated (sorry).
|
261
|
+
#
|
262
|
+
def puts( obj )
|
263
|
+
return unless writeable?
|
264
|
+
|
265
|
+
bytes = @write_io.write Marshal.dump(obj)
|
266
|
+
@write_io.write SEPERATOR if bytes > 0
|
267
|
+
@write_io.flush
|
268
|
+
|
269
|
+
bytes
|
270
|
+
end
|
271
|
+
|
272
|
+
end # class Servolux::Piper
|
273
|
+
|
274
|
+
# EOF
|
@@ -0,0 +1,226 @@
|
|
1
|
+
|
2
|
+
# == Synopsis
|
3
|
+
# The Server class makes it simple to create a server-type application in
|
4
|
+
# Ruby. A server in this context is any process that should run for a long
|
5
|
+
# period of time either in the foreground or as a daemon.
|
6
|
+
#
|
7
|
+
# == Details
|
8
|
+
# The Server class provides for standard server features: process ID file
|
9
|
+
# management, signal handling, run loop, logging, etc. All that you need to
|
10
|
+
# provide is a +run+ method that will be called by the server's run loop.
|
11
|
+
# Optionally, you can provide a block to the +new+ method and it will be
|
12
|
+
# called within the run loop instead of a run method.
|
13
|
+
#
|
14
|
+
# SIGINT and SIGTERM are handled by default. These signals will gracefully
|
15
|
+
# shutdown the server by calling the +shutdown+ method (provided by default,
|
16
|
+
# too). A few other signals can be handled by defining a few methods on your
|
17
|
+
# server instance. For example, SIGINT is hanlded by the +int+ method (an
|
18
|
+
# alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
|
19
|
+
# (another alias for +shutdown+). The following signal methods are
|
20
|
+
# recognized by the Server class:
|
21
|
+
#
|
22
|
+
# Method | Signal | Default Action
|
23
|
+
# --------+----------+----------------
|
24
|
+
# hup SIGHUP none
|
25
|
+
# int SIGINT shutdown
|
26
|
+
# term SIGTERM shutdown
|
27
|
+
# usr1 SIGUSR1 none
|
28
|
+
# usr2 SIGUSR2 none
|
29
|
+
#
|
30
|
+
# In order to handle SIGUSR1 you would define a <tt>usr1</tt> method for your
|
31
|
+
# server.
|
32
|
+
#
|
33
|
+
# There are a few other methods that are useful and should be mentioned. Two
|
34
|
+
# methods are called before and after the run loop starts: +before_starting+
|
35
|
+
# and +after_starting+. The first is called just before the run loop thread
|
36
|
+
# is created and started. The second is called just after the run loop
|
37
|
+
# thread has been created (no guarantee is made that the run loop thread has
|
38
|
+
# actually been scheduled).
|
39
|
+
#
|
40
|
+
# Likewise, two other methods are called before and after the run loop is
|
41
|
+
# shutdown: +before_stopping+ and +after_stopping+. The first is called just
|
42
|
+
# before the run loop thread is signaled for shutdown. The second is called
|
43
|
+
# just after the run loop thread has died; the +after_stopping+ method is
|
44
|
+
# guarnteed to NOT be called till after the run loop thread is well and
|
45
|
+
# truly dead.
|
46
|
+
#
|
47
|
+
# == Usage
|
48
|
+
# For simple, quick and dirty servers just pass a block to the Server
|
49
|
+
# initializer. This block will be used as the run method.
|
50
|
+
#
|
51
|
+
# server = Servolux::Server.new('Basic', :interval => 1) {
|
52
|
+
# puts "I'm alive and well @ #{Time.now}"
|
53
|
+
# }
|
54
|
+
# server.startup
|
55
|
+
#
|
56
|
+
# For more complex services you will need to define your own server methods:
|
57
|
+
# the +run+ method, signal handlers, and before/after methods. Any pattern
|
58
|
+
# that Ruby provides for defining methods on objects can be used to define
|
59
|
+
# these methods. In a nutshell:
|
60
|
+
#
|
61
|
+
# Inheritance
|
62
|
+
#
|
63
|
+
# class MyServer < Servolux::Server
|
64
|
+
# def run
|
65
|
+
# puts "I'm alive and well @ #{Time.now}"
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
# server = MyServer.new('MyServer', :interval => 1)
|
69
|
+
# server.startup
|
70
|
+
#
|
71
|
+
# Extension
|
72
|
+
#
|
73
|
+
# module MyServer
|
74
|
+
# def run
|
75
|
+
# puts "I'm alive and well @ #{Time.now}"
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# server = Servolux::Server.new('Module', :interval => 1)
|
79
|
+
# server.extend MyServer
|
80
|
+
# server.startup
|
81
|
+
#
|
82
|
+
# Singleton Class
|
83
|
+
#
|
84
|
+
# server = Servolux::Server.new('Singleton', :interval => 1)
|
85
|
+
# class << server
|
86
|
+
# def run
|
87
|
+
# puts "I'm alive and well @ #{Time.now}"
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
# server.startup
|
91
|
+
#
|
92
|
+
# == Examples
|
93
|
+
#
|
94
|
+
# === Signals
|
95
|
+
# This example shows how to change the log level of the server when SIGUSR1
|
96
|
+
# is sent to the process. The log level toggles between "debug" and the
|
97
|
+
# original log level each time SIGUSR1 is sent to the server process. Since
|
98
|
+
# this is a module, it can be used with any Servolux::Server instance.
|
99
|
+
#
|
100
|
+
# module DebugSignal
|
101
|
+
# def usr1
|
102
|
+
# if @old_log_level
|
103
|
+
# logger.level = @old_log_level
|
104
|
+
# @old_log_level = nil
|
105
|
+
# else
|
106
|
+
# @old_log_level = logger.level
|
107
|
+
# logger.level = :debug
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# server = Servolux::Server.new('Debugger', :interval => 2) {
|
113
|
+
# logger.info "Running @ #{Time.now}"
|
114
|
+
# logger.debug "hey look - a debug message"
|
115
|
+
# }
|
116
|
+
# server.extend DebugSignal
|
117
|
+
# server.startup
|
118
|
+
#
|
119
|
+
class Servolux::Server
|
120
|
+
include ::Servolux::Threaded
|
121
|
+
|
122
|
+
# :stopdoc:
|
123
|
+
SIGNALS = %w[HUP INT TERM USR1 USR2] & Signal.list.keys
|
124
|
+
SIGNALS.each {|sig| sig.freeze}.freeze
|
125
|
+
# :startdoc:
|
126
|
+
|
127
|
+
Error = Class.new(::Servolux::Error)
|
128
|
+
|
129
|
+
attr_reader :name
|
130
|
+
attr_writer :logger
|
131
|
+
attr_writer :pid_file
|
132
|
+
|
133
|
+
# call-seq:
|
134
|
+
# Server.new( name, options = {} ) { block }
|
135
|
+
#
|
136
|
+
# Creates a new server identified by _name_ and configured from the
|
137
|
+
# _options_ hash. The _block_ is run inside a separate thread that will
|
138
|
+
# loop at the configured interval.
|
139
|
+
#
|
140
|
+
# ==== Options
|
141
|
+
# * logger <Logger> :: The logger instance this server will use
|
142
|
+
# * pid_file <String> :: Location of the PID file
|
143
|
+
# * interval <Numeric> :: Sleep interval between invocations of the _block_
|
144
|
+
#
|
145
|
+
def initialize( name, opts = {}, &block )
|
146
|
+
@name = name
|
147
|
+
|
148
|
+
self.logger = opts.getopt :logger
|
149
|
+
self.pid_file = opts.getopt :pid_file
|
150
|
+
self.interval = opts.getopt :interval, 0
|
151
|
+
|
152
|
+
if block
|
153
|
+
eg = class << self; self; end
|
154
|
+
eg.__send__(:define_method, :run, &block)
|
155
|
+
end
|
156
|
+
|
157
|
+
ary = %w[name logger pid_file].map { |var|
|
158
|
+
self.send(var).nil? ? var : nil
|
159
|
+
}.compact
|
160
|
+
raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
|
161
|
+
end
|
162
|
+
|
163
|
+
# Start the server running using it's own internal thread. This method
|
164
|
+
# will not return until the server is shutdown.
|
165
|
+
#
|
166
|
+
# Startup involves creating a PID file, registering signal handlers to
|
167
|
+
# shutdown the server, starting and joining the server thread. The PID
|
168
|
+
# file is deleted when this method returns.
|
169
|
+
#
|
170
|
+
def startup
|
171
|
+
return self if running?
|
172
|
+
begin
|
173
|
+
create_pid_file
|
174
|
+
trap_signals
|
175
|
+
start
|
176
|
+
join
|
177
|
+
ensure
|
178
|
+
delete_pid_file
|
179
|
+
end
|
180
|
+
return self
|
181
|
+
end
|
182
|
+
|
183
|
+
alias :shutdown :stop # for symmetry with the startup method
|
184
|
+
alias :int :stop # handles the INT signal
|
185
|
+
alias :term :stop # handles the TERM signal
|
186
|
+
private :start, :stop
|
187
|
+
|
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
|
+
# Returns the PID file name used by the server. If none was given, then
|
197
|
+
# the server name is used to create a PID file name.
|
198
|
+
#
|
199
|
+
def pid_file
|
200
|
+
@pid_file ||= name.downcase.tr(' ','_') + '.pid'
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def create_pid_file
|
206
|
+
logger.debug "Server #{name.inspect} creating pid file #{pid_file.inspect}"
|
207
|
+
File.open(pid_file, 'w') {|fd| fd.write(Process.pid.to_s)}
|
208
|
+
end
|
209
|
+
|
210
|
+
def delete_pid_file
|
211
|
+
if test(?f, pid_file)
|
212
|
+
logger.debug "Server #{name.inspect} removing pid file #{pid_file.inspect}"
|
213
|
+
File.delete(pid_file)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def trap_signals
|
218
|
+
SIGNALS.each do |sig|
|
219
|
+
m = sig.downcase.to_sym
|
220
|
+
Signal.trap(sig) { self.send(m) rescue nil } if self.respond_to? m
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end # class Servolux::Server
|
225
|
+
|
226
|
+
# EOF
|
@@ -0,0 +1,133 @@
|
|
1
|
+
|
2
|
+
# == Synopsis
|
3
|
+
# The Threaded module is used to peform some activity at a specified
|
4
|
+
# interval.
|
5
|
+
#
|
6
|
+
# == Details
|
7
|
+
# Sometimes it is useful for an object to have its own thread of execution
|
8
|
+
# to perform a task at a recurring interval. The Threaded module
|
9
|
+
# encapsulates this functionality so you don't have to write it yourself. It
|
10
|
+
# can be used with any object that responds to the +run+ method.
|
11
|
+
#
|
12
|
+
# The threaded object is run by calling the +start+ method. This will create
|
13
|
+
# a new thread that will invoke the +run+ method at the desired interval.
|
14
|
+
# Just before the thread is created the +before_starting+ method will be
|
15
|
+
# called (if it is defined by the threaded object). Likewise, after the
|
16
|
+
# thread is created the +after_starting+ method will be called (if it is
|
17
|
+
# defeined by the threaded object).
|
18
|
+
#
|
19
|
+
# The threaded object is stopped by calling the +stop+ method. This sets an
|
20
|
+
# internal flag and then wakes up the thread. The thread gracefully exits
|
21
|
+
# after checking the flag. Like the start method, before and after methods
|
22
|
+
# are defined for stopping as well. Just before the thread is stopped the
|
23
|
+
# +before_stopping+ method will be called (if it is defined by the threaded
|
24
|
+
# object). Likewise, after the thread has died the +after_stopping+ method
|
25
|
+
# will be called (if it is defeined by the threaded object).
|
26
|
+
#
|
27
|
+
# Calling the +join+ method on a threaded object will cause the calling
|
28
|
+
# thread to wait until the threaded object has stopped. An optional timeout
|
29
|
+
# parameter can be given.
|
30
|
+
#
|
31
|
+
# == Examples
|
32
|
+
# Take a look at the Servolux::Server class for an example of a threaded
|
33
|
+
# object.
|
34
|
+
#
|
35
|
+
module Servolux::Threaded
|
36
|
+
|
37
|
+
# This method will be called by the activity thread at the desired
|
38
|
+
# interval. Implementing classes are exptect to provide this
|
39
|
+
# functionality.
|
40
|
+
#
|
41
|
+
def run
|
42
|
+
raise NotImplementedError,
|
43
|
+
'This method must be defined by the threaded object.'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Start the activity thread. If already started this method will return
|
47
|
+
# without taking any action.
|
48
|
+
#
|
49
|
+
# If the including class defines a 'before_starting' method, it will be
|
50
|
+
# called before the thread is created and run. Likewise, if the
|
51
|
+
# including class defines an 'after_starting' method, it will be called
|
52
|
+
# after the thread is created.
|
53
|
+
#
|
54
|
+
def start
|
55
|
+
return self if running?
|
56
|
+
logger.debug "Starting"
|
57
|
+
|
58
|
+
before_starting if self.respond_to?(:before_starting)
|
59
|
+
@activity_thread_running = true
|
60
|
+
@activity_thread = Thread.new {
|
61
|
+
begin
|
62
|
+
loop {
|
63
|
+
sleep interval
|
64
|
+
break unless running?
|
65
|
+
run
|
66
|
+
}
|
67
|
+
rescue Exception => e
|
68
|
+
logger.fatal e
|
69
|
+
end
|
70
|
+
}
|
71
|
+
after_starting if self.respond_to?(:after_starting)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Stop the activity thread. If already stopped this method will return
|
76
|
+
# without taking any action.
|
77
|
+
#
|
78
|
+
# If the including class defines a 'before_stopping' method, it will be
|
79
|
+
# called before the thread is stopped. Likewise, if the including class
|
80
|
+
# defines an 'after_stopping' method, it will be called after the thread
|
81
|
+
# has stopped.
|
82
|
+
#
|
83
|
+
def stop
|
84
|
+
return self unless running?
|
85
|
+
logger.debug "Stopping"
|
86
|
+
|
87
|
+
before_stopping if self.respond_to?(:before_stopping)
|
88
|
+
@activity_thread_running = false
|
89
|
+
@activity_thread.wakeup
|
90
|
+
@activity_thread.join
|
91
|
+
@activity_thread = nil
|
92
|
+
after_stopping if self.respond_to?(:after_stopping)
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
# If the activity thread is running, the calling thread will suspend
|
97
|
+
# execution and run the activity thread. This method does not return until
|
98
|
+
# the activity thread is stopped or until _limit_ seconds have passed.
|
99
|
+
#
|
100
|
+
# If the activity thread is not running, this method returns immediately
|
101
|
+
# with +nil+.
|
102
|
+
#
|
103
|
+
def join( limit = nil )
|
104
|
+
@activity_thread.join limit
|
105
|
+
self
|
106
|
+
rescue NoMethodError
|
107
|
+
return self
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns +true+ if the activity thread is running. Returns +false+
|
111
|
+
# otherwise.
|
112
|
+
#
|
113
|
+
def running?
|
114
|
+
@activity_thread_running
|
115
|
+
end
|
116
|
+
|
117
|
+
# Sets the number of seconds to sleep between invocations of the
|
118
|
+
# threaded object's 'run' method.
|
119
|
+
#
|
120
|
+
def interval=( value )
|
121
|
+
@activity_thread_interval = value
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the number of seconds to sleep between invocations of the
|
125
|
+
# threaded object's 'run' method.
|
126
|
+
#
|
127
|
+
def interval
|
128
|
+
@activity_thread_interval
|
129
|
+
end
|
130
|
+
|
131
|
+
end # module Servolux::Threaded
|
132
|
+
|
133
|
+
# EOF
|
data/lib/servolux.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
require 'logging'
|
3
|
+
|
4
|
+
module Servolux
|
5
|
+
|
6
|
+
# :stopdoc:
|
7
|
+
VERSION = '0.1.0'
|
8
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
9
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
10
|
+
# :startdoc:
|
11
|
+
|
12
|
+
# Generic Servolux Error class.
|
13
|
+
Error = Class.new(StandardError)
|
14
|
+
|
15
|
+
# Returns the version string for the library.
|
16
|
+
#
|
17
|
+
def self.version
|
18
|
+
VERSION
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the library path for the module. If any arguments are given,
|
22
|
+
# they will be joined to the end of the libray path using
|
23
|
+
# <tt>File.join</tt>.
|
24
|
+
#
|
25
|
+
def self.libpath( *args )
|
26
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the lpath for the module. If any arguments are given,
|
30
|
+
# they will be joined to the end of the path using
|
31
|
+
# <tt>File.join</tt>.
|
32
|
+
#
|
33
|
+
def self.path( *args )
|
34
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns +true+ if the execution platform supports fork.
|
38
|
+
#
|
39
|
+
def self.fork?
|
40
|
+
RUBY_PLATFORM != 'java' and test(?e, '/dev/null')
|
41
|
+
end
|
42
|
+
|
43
|
+
end # module Servolux
|
44
|
+
|
45
|
+
%w[threaded server piper daemon].each do |lib|
|
46
|
+
require Servolux.libpath('servolux', lib)
|
47
|
+
end
|
48
|
+
|
49
|
+
# EOF
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
3
|
+
|
4
|
+
describe Servolux do
|
5
|
+
|
6
|
+
before :all do
|
7
|
+
@root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
8
|
+
end
|
9
|
+
|
10
|
+
it "finds things releative to 'lib'" do
|
11
|
+
Servolux.libpath(%w[servolux threaded]).should == File.join(@root_dir, %w[lib servolux threaded])
|
12
|
+
end
|
13
|
+
|
14
|
+
it "finds things releative to 'root'" do
|
15
|
+
Servolux.path('Rakefile').should == File.join(@root_dir, 'Rakefile')
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# EOF
|