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