servolux 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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