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
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= Serv-O-Lux
|
2
|
+
* by Tim Pease
|
3
|
+
* http://codeforpeople.rubyforge.org/servolux
|
4
|
+
|
5
|
+
=== DESCRIPTION:
|
6
|
+
|
7
|
+
Threads : Servers : Forks : Daemons
|
8
|
+
|
9
|
+
=== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* FIXME (list of features or problems)
|
12
|
+
|
13
|
+
=== SYNOPSIS:
|
14
|
+
|
15
|
+
FIXME (code sample of usage)
|
16
|
+
|
17
|
+
=== REQUIREMENTS:
|
18
|
+
|
19
|
+
* FIXME (list of requirements)
|
20
|
+
|
21
|
+
=== INSTALL:
|
22
|
+
|
23
|
+
gem install servolux
|
24
|
+
|
25
|
+
=== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2009
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
begin
|
10
|
+
load 'tasks/setup.rb'
|
11
|
+
rescue LoadError
|
12
|
+
raise RuntimeError, '### please install the "bones" gem ###'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ensure_in_path 'lib'
|
17
|
+
require 'servolux'
|
18
|
+
|
19
|
+
task :default => 'spec:specdoc'
|
20
|
+
|
21
|
+
PROJ.name = 'servolux'
|
22
|
+
PROJ.authors = 'Tim Pease'
|
23
|
+
PROJ.email = 'tim.pease@gmail.com'
|
24
|
+
PROJ.url = 'http://codeforpeople.rubyforge.org/servolux'
|
25
|
+
PROJ.version = Servolux::VERSION
|
26
|
+
PROJ.rubyforge.name = 'codeforpeople'
|
27
|
+
PROJ.exclude << 'servolux.gemspec'
|
28
|
+
PROJ.readme_file = 'README.rdoc'
|
29
|
+
PROJ.ignore_file = '.gitignore'
|
30
|
+
PROJ.rdoc.remote_dir = 'servolux'
|
31
|
+
|
32
|
+
PROJ.spec.opts << '--color'
|
33
|
+
|
34
|
+
PROJ.ann.email[:server] = 'smtp.gmail.com'
|
35
|
+
PROJ.ann.email[:port] = 587
|
36
|
+
PROJ.ann.email[:from] = 'Tim Pease'
|
37
|
+
|
38
|
+
depend_on 'logging'
|
39
|
+
depend_on 'rspec'
|
40
|
+
|
41
|
+
# EOF
|
@@ -0,0 +1,403 @@
|
|
1
|
+
|
2
|
+
# == Synopsis
|
3
|
+
# The Daemon takes care of the work of creating and managing daemon
|
4
|
+
# processes from with Ruby.
|
5
|
+
#
|
6
|
+
class Servolux::Daemon
|
7
|
+
|
8
|
+
Error = Class.new(StandardError)
|
9
|
+
Timeout = Class.new(Error)
|
10
|
+
AlreadyStarted = Class.new(Error)
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
attr_writer :logger
|
14
|
+
attr_accessor :pid_file
|
15
|
+
attr_reader :startup_command
|
16
|
+
attr_accessor :shutdown_command
|
17
|
+
attr_accessor :timeout
|
18
|
+
attr_accessor :nochdir
|
19
|
+
attr_accessor :noclose
|
20
|
+
attr_reader :log_file
|
21
|
+
attr_reader :look_for
|
22
|
+
|
23
|
+
# Create a new Daemon that will manage the +startup_command+ as a deamon
|
24
|
+
# process.
|
25
|
+
#
|
26
|
+
# ==== Required
|
27
|
+
# * name <String>
|
28
|
+
# The name of the daemon process. This name will appear in log
|
29
|
+
# messages.
|
30
|
+
#
|
31
|
+
# * logger <Logger>
|
32
|
+
# The Logger instance used to output messages.
|
33
|
+
#
|
34
|
+
# * pid_file <String>
|
35
|
+
# Location of the PID file. This is used to determine if the daemon
|
36
|
+
# process is running, and to send signals to the daemon process.
|
37
|
+
#
|
38
|
+
# * startup_command
|
39
|
+
# Assign the startup command. This can be either a String, an Array of
|
40
|
+
# strings, a Proc, a bound Method, or a Servolux::Server instance.
|
41
|
+
# Different calling semantics are used for each type of command. See
|
42
|
+
# the setter method for more details.
|
43
|
+
#
|
44
|
+
# ==== Options
|
45
|
+
#
|
46
|
+
# * timeout <Numeric>
|
47
|
+
# The time (in seconds) to wait for the daemon process to either
|
48
|
+
# startup or shutdown. An error is raised when this timeout is
|
49
|
+
# exceeded. The default is 30 seconds.
|
50
|
+
#
|
51
|
+
# * nochdir <Boolean>
|
52
|
+
# When set to true this flag directs the daemon process to keep the
|
53
|
+
# current working directory. By default, the process of daemonizing
|
54
|
+
# will cause the current working directory to be changed to the root
|
55
|
+
# folder (thus preventing the daemon process from holding onto the
|
56
|
+
# directory inode). The default is false.
|
57
|
+
#
|
58
|
+
# * noclose <Boolean>
|
59
|
+
# When set to true this flag keeps the standard input/output streams
|
60
|
+
# from being reopend to /dev/null when the deamon process is created.
|
61
|
+
# Reopening the standard input/output streams frees the file
|
62
|
+
# descriptors which are still being used by the parent process. This
|
63
|
+
# prevents zombie processes. The default is false.
|
64
|
+
#
|
65
|
+
# * shutdown_command
|
66
|
+
# TODO: Not Yet Implemented
|
67
|
+
#
|
68
|
+
# * log_file <String>
|
69
|
+
# This log file will be monitored to determine if the daemon process
|
70
|
+
# has sucessfully started.
|
71
|
+
#
|
72
|
+
# * look_for
|
73
|
+
# This can be either a String or a Regexp. It defines a phrase to
|
74
|
+
# search for in the log_file. When the daemon process is started, the
|
75
|
+
# parent process will not return until this phrase is found in the log
|
76
|
+
# file. This is a useful check for determining if the daemon process
|
77
|
+
# is fully started. The default is nil.
|
78
|
+
#
|
79
|
+
def initialize( opts = {} )
|
80
|
+
self.server = opts.getopt(:server) || opts.getopt(:startup_command)
|
81
|
+
|
82
|
+
@name = opts[:name] if opts.key?(:name)
|
83
|
+
@logger = opts[:logger] if opts.key?(:logger)
|
84
|
+
@pid_file = opts[:pid_file] if opts.key?(:pid_file)
|
85
|
+
@timeout = opts.getopt(:timeout, 30)
|
86
|
+
@nochdir = opts.getopt(:nochdir, false)
|
87
|
+
@noclose = opts.getopt(:noclose, false)
|
88
|
+
@shutdown_command = opts.getopt(:shutdown_command)
|
89
|
+
|
90
|
+
@piper = nil
|
91
|
+
@logfile_reader = nil
|
92
|
+
self.log_file = opts.getopt(:log_file)
|
93
|
+
self.look_for = opts.getopt(:look_for)
|
94
|
+
|
95
|
+
yield self if block_given?
|
96
|
+
|
97
|
+
ary = %w[name logger pid_file startup_command].map { |var|
|
98
|
+
self.send(var).nil? ? var : nil
|
99
|
+
}.compact
|
100
|
+
raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Assign the startup command. This can be either a String, an Array of
|
104
|
+
# strings, a Proc, a bound Method, or a Servolux::Server instance.
|
105
|
+
# Different calling semantics are used for each type of command.
|
106
|
+
#
|
107
|
+
# If the startup command is a String or an Array of strings, then
|
108
|
+
# Kernel#exec is used to run the command. Therefore, the string (or array)
|
109
|
+
# should be system level command that is either fully qualified or can be
|
110
|
+
# found on the current environment path.
|
111
|
+
#
|
112
|
+
# If the startup command is a Proc or a bound Method then it is invoked
|
113
|
+
# using the +call+ method on the object. No arguments are passed to the
|
114
|
+
# +call+ invocoation.
|
115
|
+
#
|
116
|
+
# Lastly, if the startup command is a Servolux::Server then it's +startup+
|
117
|
+
# method is called.
|
118
|
+
#
|
119
|
+
def startup_command=( val )
|
120
|
+
@startup_command = val
|
121
|
+
return unless val.is_a?(::Servolux::Server)
|
122
|
+
|
123
|
+
@name = val.name
|
124
|
+
@logger = val.logger
|
125
|
+
@pid_file = val.pid_file
|
126
|
+
@shutdown_command = nil
|
127
|
+
end
|
128
|
+
alias :server= :startup_command=
|
129
|
+
alias :server :startup_command
|
130
|
+
|
131
|
+
# Assign the log file name. This log file will be monitored to determine
|
132
|
+
# if the daemon process is running.
|
133
|
+
#
|
134
|
+
def log_file=( filename )
|
135
|
+
return if filename.nil?
|
136
|
+
@logfile_reader ||= LogfileReader.new
|
137
|
+
@logfile_reader.filename = filename
|
138
|
+
end
|
139
|
+
|
140
|
+
# A string or regular expression to search for in the log file. When the
|
141
|
+
# daemon process is started, the parent process will not return until this
|
142
|
+
# phrase is found in the log file. This is a useful check for determining
|
143
|
+
# if the daemon process is fully started.
|
144
|
+
#
|
145
|
+
# If no phrase is given to look for, then the log file will simply be
|
146
|
+
# watched for a change in size and a modified timestamp.
|
147
|
+
#
|
148
|
+
def look_for=( val )
|
149
|
+
return if val.nil?
|
150
|
+
@logfile_reader ||= LogfileReader.new
|
151
|
+
@logfile_reader.look_for = val
|
152
|
+
end
|
153
|
+
|
154
|
+
# Start the daemon process.
|
155
|
+
#
|
156
|
+
def startup
|
157
|
+
raise Error, "Fork is not supported in this Ruby environment." unless ::Servolux.fork?
|
158
|
+
|
159
|
+
if alive?
|
160
|
+
raise AlreadyStarted,
|
161
|
+
"#{name.inspect} is already running: " \
|
162
|
+
"PID is #{retrieve_pid} from PID file #{pid_file.inspect}"
|
163
|
+
end
|
164
|
+
|
165
|
+
daemonize
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns +true+ if the daemon processis currently running. Returns
|
169
|
+
# +false+ if this is not the case. The status of the process is determined
|
170
|
+
# by sending a signal to the process identified by the +pid_file+.
|
171
|
+
#
|
172
|
+
def alive?
|
173
|
+
pid = retrieve_pid
|
174
|
+
Process.kill(0, pid)
|
175
|
+
true
|
176
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
177
|
+
false
|
178
|
+
rescue Errno::EACCES => err
|
179
|
+
logger.error "You do not have access to the PID file at " \
|
180
|
+
"#{pid_file.inspect}: #{err.message}"
|
181
|
+
false
|
182
|
+
end
|
183
|
+
|
184
|
+
# Send a signal to the daemon process identified by the PID file. The
|
185
|
+
# default signal to send is 'INT' (2). The signal can be given either as a
|
186
|
+
# string or a signal number.
|
187
|
+
#
|
188
|
+
def kill( signal = 'INT' )
|
189
|
+
signal = Signal.list.invert[signal] if signal.is_a?(Integer)
|
190
|
+
pid = retrieve_pid
|
191
|
+
logger.info "Killing PID #{pid} with #{signal}"
|
192
|
+
Process.kill(signal, pid)
|
193
|
+
wait_for_shutdown
|
194
|
+
rescue Errno::EINVAL
|
195
|
+
logger.error "Failed to kill PID #{pid} with #{signal}: " \
|
196
|
+
"'#{signal}' is an invalid or unsupported signal number."
|
197
|
+
rescue Errno::EPERM
|
198
|
+
logger.error "Failed to kill PID #{pid} with #{signal}: " \
|
199
|
+
"Insufficient permissions."
|
200
|
+
rescue Errno::ESRCH
|
201
|
+
logger.error "Failed to kill PID #{pid} with #{signal}: " \
|
202
|
+
"Process is deceased or zombie."
|
203
|
+
rescue Errno::EACCES => err
|
204
|
+
logger.error err.message
|
205
|
+
rescue Errno::ENOENT => err
|
206
|
+
logger.error "Could not find a PID file at #{pid_file.inspect}. " \
|
207
|
+
"Most likely the process is no longer running."
|
208
|
+
rescue Exception => err
|
209
|
+
unless err.is_a?(SystemExit)
|
210
|
+
logger.error "Failed to kill PID #{pid} with #{signal}: #{err.message}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the logger instance used by the daemon to log messages.
|
215
|
+
#
|
216
|
+
def logger
|
217
|
+
@logger ||= Logging.logger[self]
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
private
|
222
|
+
|
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
|
+
def run_startup_command
|
237
|
+
case startup_command
|
238
|
+
when String; exec(startup_command)
|
239
|
+
when Array; exec(*startup_command)
|
240
|
+
when Proc, Method; startup_command.call
|
241
|
+
when ::Servolux::Server; startup_command.startup
|
242
|
+
else
|
243
|
+
raise Error, "Unrecognized startup command #{startup_command.inspect}"
|
244
|
+
end
|
245
|
+
|
246
|
+
rescue Exception => err
|
247
|
+
unless err.is_a?(SystemExit)
|
248
|
+
logger.fatal err
|
249
|
+
@piper.puts err
|
250
|
+
end
|
251
|
+
ensure
|
252
|
+
@piper.close
|
253
|
+
end
|
254
|
+
|
255
|
+
def exec( *args )
|
256
|
+
logger.debug "Calling: exec(*#{args.inspect})"
|
257
|
+
std = [STDIN, STDOUT, STDERR, @piper.write_io]
|
258
|
+
ObjectSpace.each_object(IO) { |obj|
|
259
|
+
next if std.include? obj
|
260
|
+
obj.close rescue nil
|
261
|
+
}
|
262
|
+
Kernel.exec(*args)
|
263
|
+
end
|
264
|
+
|
265
|
+
def retrieve_pid
|
266
|
+
@piper ? @piper.pid : Integer(File.read(pid_file).strip)
|
267
|
+
rescue TypeError
|
268
|
+
raise Error, "A PID file was not specified."
|
269
|
+
rescue ArgumentError
|
270
|
+
raise Error, "#{pid_file.inspect} does not contain a valid PID."
|
271
|
+
end
|
272
|
+
|
273
|
+
def started?
|
274
|
+
return false unless alive?
|
275
|
+
return true if @logfile_reader.nil?
|
276
|
+
@logfile_reader.updated?
|
277
|
+
end
|
278
|
+
|
279
|
+
def wait_for_startup
|
280
|
+
logger.debug "Waiting for #{name.inspect} to startup."
|
281
|
+
|
282
|
+
begin
|
283
|
+
started = wait_for {
|
284
|
+
rv = started?
|
285
|
+
err = @piper.gets; raise err unless err.nil?
|
286
|
+
rv
|
287
|
+
}
|
288
|
+
|
289
|
+
if started
|
290
|
+
logger.info 'Server has daemonized.'
|
291
|
+
return
|
292
|
+
end
|
293
|
+
rescue Exception => err
|
294
|
+
child_err = @piper.gets
|
295
|
+
raise child_err || err
|
296
|
+
ensure
|
297
|
+
@piper.close
|
298
|
+
end
|
299
|
+
|
300
|
+
# if the daemon doesn't fork into the background in time, then kill it.
|
301
|
+
force_kill
|
302
|
+
end
|
303
|
+
|
304
|
+
def force_kill
|
305
|
+
pid = retrieve_pid
|
306
|
+
|
307
|
+
t = Thread.new {
|
308
|
+
begin
|
309
|
+
sleep 7
|
310
|
+
unless Thread.current[:stop]
|
311
|
+
Process.kill('KILL', pid)
|
312
|
+
Process.waitpid(pid)
|
313
|
+
end
|
314
|
+
rescue Exception
|
315
|
+
end
|
316
|
+
}
|
317
|
+
|
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
|
+
raise Timeout, "#{name.inspect} failed to startup in a timely fashion. " \
|
325
|
+
"The timeout is set at #{timeout} seconds."
|
326
|
+
|
327
|
+
rescue Errno::ENOENT
|
328
|
+
raise Timeout, "Could not find a PID file at #{pid_file.inspect}."
|
329
|
+
rescue Errno::EACCES => err
|
330
|
+
raise Timeout, "You do not have access to the PID file at " \
|
331
|
+
"#{pid_file.inspect}: #{err.message}"
|
332
|
+
end
|
333
|
+
|
334
|
+
def wait_for_shutdown
|
335
|
+
logger.debug "Waiting for #{name.inspect} to shutdown."
|
336
|
+
return if wait_for { !alive? }
|
337
|
+
raise Timeout, "#{name.inspect} failed to shutdown in a timely fashion. " \
|
338
|
+
"The timeout is set at #{timeout} seconds."
|
339
|
+
end
|
340
|
+
|
341
|
+
def wait_for
|
342
|
+
start = Time.now
|
343
|
+
nap_time = 0.1
|
344
|
+
|
345
|
+
loop do
|
346
|
+
sleep nap_time
|
347
|
+
|
348
|
+
diff = Time.now - start
|
349
|
+
nap_time = 2*nap_time
|
350
|
+
nap_time = diff + 0.1 if diff < nap_time
|
351
|
+
|
352
|
+
break true if yield
|
353
|
+
break false if diff >= timeout
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# :stopdoc:
|
358
|
+
class LogfileReader
|
359
|
+
attr_accessor :filename
|
360
|
+
attr_reader :look_for
|
361
|
+
|
362
|
+
def look_for=( val )
|
363
|
+
case val
|
364
|
+
when nil; @look_for = nil
|
365
|
+
when String; @look_for = Regexp.new(Regexp.escape(val))
|
366
|
+
when Regexp; @look_for = val
|
367
|
+
else
|
368
|
+
raise Error,
|
369
|
+
"Don't know how to look for #{val.inspect} in the logfile"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def stat
|
374
|
+
if @filename and test(?f, @filename)
|
375
|
+
File.stat @filename
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def updated?
|
380
|
+
s = stat
|
381
|
+
@stat ||= s
|
382
|
+
|
383
|
+
return false if s.nil?
|
384
|
+
return false if @stat.mtime == s.mtime and @stat.size == s.size
|
385
|
+
return true if @look_for.nil?
|
386
|
+
|
387
|
+
File.open(@filename, 'r') do |fd|
|
388
|
+
fd.seek @stat.size, IO::SEEK_SET
|
389
|
+
while line = fd.gets
|
390
|
+
return true if line =~ @look_for
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
return false
|
395
|
+
ensure
|
396
|
+
@stat = s
|
397
|
+
end
|
398
|
+
end # class LogfileReader
|
399
|
+
# :startdoc:
|
400
|
+
|
401
|
+
end # class Servolux::Daemon
|
402
|
+
|
403
|
+
# EOF
|