servolux 0.1.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 +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
|