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 ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 / 2009-06-18
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
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