FooBarWidget-daemon_controller 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/README.rdoc ADDED
@@ -0,0 +1,41 @@
1
+ = Introduction
2
+
3
+ daemon_controller is a library for implementing daemon management capabilities.
4
+
5
+ Suppose that you have a Ruby on Rails application which uses the Sphinx search
6
+ server [1] for full-text searching capbilities. In order to search the index,
7
+ the search daemon (searchd) must be running. Furthermore, you're using the Riddle
8
+ library [2] for interfacing with the search daemon.
9
+
10
+ You can write this in your application:
11
+
12
+ require 'daemon_controller'
13
+ require 'riddle'
14
+
15
+ controller = DaemonController.new(
16
+ :identifier => 'Sphinx search daemon',
17
+ :start_command => 'searchd -c config/sphinx.conf',
18
+ :ping_command => proc { Riddle::Client.new('localhost', 1234) },
19
+ :pid_file => 'tmp/pids/sphinx.pid',
20
+ :log_file => 'log/sphinx.log'
21
+ )
22
+ client = controller.connect do
23
+ Riddle::Client.new('localhost', 1234)
24
+ end
25
+ client.query("some search query...")
26
+
27
+ controller.connect will start the Sphinx search daemon if it isn't already
28
+ started. Then, it will connect to the Sphinx search daemon by running the
29
+ given block.
30
+
31
+ Basically you just tell the library how to start the daemon, how to check
32
+ whether it's responding to connections, and which PID file and log file it
33
+ uses. daemon_controller will automatically take care of things like:
34
+
35
+ * concurrency control, e.g. to ensure that no two processes will try to start
36
+ the Sphinx search daemon at the same time.
37
+ * error handling: if 'searchd' failed to start, then its error message will
38
+ be propagated into the exception that will be thrown. This makes it much
39
+ easier to handle daemon startup errors in your application. This can also
40
+ allow the system administrator to see the error message directly in your
41
+ application, instead of having to consult the daemon's log file.
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "daemon_controller"
3
+ s.version = "0.1.0"
4
+ s.date = "2008-08-21"
5
+ s.summary = "A library for implementing daemon management capabilities"
6
+ s.email = "hongli@phusion.nl"
7
+ s.homepage = "http://github.com/FooBarWidget/daemon_controller/tree/master"
8
+ s.description = "A library for implementing daemon management capabilities."
9
+ s.has_rdoc = false
10
+ s.authors = ["Hongli Lai"]
11
+
12
+ s.files = [
13
+ "README.rdoc", "LICENSE.txt", "daemon_controller.gemspec",
14
+ "lib/daemon_controller.rb",
15
+ "spec/daemon_controller_spec.rb",
16
+ "spec/echo_server.rb"
17
+ ]
18
+ end
@@ -0,0 +1,525 @@
1
+ # Basic functionality for a single, local, external daemon:
2
+ # - starting daemon
3
+ # * must be concurrency-safe!
4
+ # * must be able to report startup errors!
5
+ # * returns when daemon is fully operational
6
+ # - stopping daemon
7
+ # * must be concurrency-safe!
8
+ # * returns when daemon has exited
9
+ # - querying the status of a daemon
10
+ # * querying the status of a daemon (i.e. whether it's running)
11
+ # - connect to a daemon, and start it if it isn't already running
12
+ # * must be a single atomic action
13
+
14
+ require 'tempfile'
15
+ require 'fcntl'
16
+
17
+ class DaemonController
18
+ class Error < StandardError
19
+ end
20
+ class TimeoutError < Error
21
+ end
22
+ class AlreadyStarted < Error
23
+ end
24
+ class StartError < Error
25
+ end
26
+ class StartTimeout < TimeoutError
27
+ end
28
+ class StopError < Error
29
+ end
30
+ class StopTimeout < TimeoutError
31
+ end
32
+ class ConnectError < Error
33
+ end
34
+
35
+ # Create a new DaemonController object.
36
+ #
37
+ # === Mandatory options
38
+ #
39
+ # [:identifier]
40
+ # A human-readable, unique name for this daemon, e.g. "Sphinx search server".
41
+ # This identifier will be used in some error messages. On some platforms, it will
42
+ # be used for concurrency control: on such platforms, no two DaemonController
43
+ # objects will operate on the same identifier on the same time.
44
+ #
45
+ # [:start_command]
46
+ # The command to start the daemon. This must be a a String, e.g.
47
+ # "mongrel_rails start -e production".
48
+ #
49
+ # [:ping_command]
50
+ # The ping command is used to check whether the daemon can be connected to.
51
+ # It is also used to ensure that #start only returns when the daemon can be
52
+ # connected to.
53
+ #
54
+ # The value may be a command string. This command must exit with an exit code of
55
+ # 0 if the daemon can be successfully connected to, or exit with a non-0 exit
56
+ # code on failure.
57
+ #
58
+ # The value may also be a Proc, which returns an expression that evaluates to
59
+ # true (indicating that the daemon can be connected to) or false (failure).
60
+ #
61
+ # [:pid_file]
62
+ # The PID file that the daemon will write to. Used to check whether the daemon
63
+ # is running.
64
+ #
65
+ # [:log_file]
66
+ # The log file that the daemon will write to. It will be consulted to see
67
+ # whether the daemon has printed any error messages during startup.
68
+ #
69
+ # === Optional options
70
+ # [:stop_command]
71
+ # A command to stop the daemon with, e.g. "/etc/rc.d/nginx stop". If no stop
72
+ # command is given (i.e. +nil+), then DaemonController will stop the daemon
73
+ # by killing the PID written in the PID file.
74
+ #
75
+ # The default value is +nil+.
76
+ #
77
+ # [:start_timeout]
78
+ # The maximum amount of time, in seconds, that #start may take to start
79
+ # the daemon. Since #start also waits until the daemon can be connected to,
80
+ # that wait time is counted as well. If the daemon does not start in time,
81
+ # then #start will raise an exception.
82
+ #
83
+ # The default value is 15.
84
+ #
85
+ # [:stop_timeout]
86
+ # The maximum amount of time, in seconds, that #stop may take to stop
87
+ # the daemon. Since #stop also waits until the daemon is no longer running,
88
+ # that wait time is counted as well. If the daemon does not stop in time,
89
+ # then #stop will raise an exception.
90
+ #
91
+ # The default value is 15.
92
+ #
93
+ # [:log_file_activity_timeout]
94
+ # Once a daemon has gone into the background, it will become difficult to
95
+ # know for certain whether it is still initializing or whether it has
96
+ # failed and exited, until it has written its PID file. It's 99.9% probable
97
+ # that the daemon has terminated with an if its start timeout has expired,
98
+ # not many system administrators want to wait 15 seconds (the default start
99
+ # timeout) to be notified of whether the daemon has terminated with an error.
100
+ #
101
+ # An alternative way to check whether the daemon has terminated with an error,
102
+ # is by checking whether its log file has been recently updated. If, after the
103
+ # daemon has started, the log file hasn't been updated for the amount of seconds
104
+ # given by the :log_file_activity_timeout option, then the daemon is assumed to
105
+ # have terminated with an error.
106
+ #
107
+ # The default value is 7.
108
+ def initialize(options)
109
+ [:identifier, :start_command, :ping_command, :pid_file, :log_file].each do |option|
110
+ if !options.has_key?(option)
111
+ raise ArgumentError, "The ':#{option}' option is mandatory."
112
+ end
113
+ end
114
+ @identifier = options[:identifier]
115
+ @start_command = options[:start_command]
116
+ @stop_command = options[:stop_command]
117
+ @ping_command = options[:ping_command]
118
+ @ping_interval = options[:ping_interval] || 0.1
119
+ @pid_file = options[:pid_file]
120
+ @log_file = options[:log_file]
121
+ @start_timeout = options[:start_timeout] || 15
122
+ @stop_timeout = options[:stop_timeout] || 15
123
+ @log_file_activity_timeout = options[:log_file_activity_timeout] || 7
124
+ @lock_file = determine_lock_file(@identifier, @pid_file)
125
+ end
126
+
127
+ # Start the daemon and wait until it can be pinged.
128
+ #
129
+ # Raises:
130
+ # - AlreadyStarted - the daemon is already running.
131
+ # - StartError - the start command failed.
132
+ # - StartTimeout - the daemon did not start in time. This could also
133
+ # mean that the daemon failed after it has gone into the background.
134
+ def start
135
+ exclusive_lock do
136
+ start_without_locking
137
+ end
138
+ end
139
+
140
+ # Connect to the daemon by running the given block, which contains the
141
+ # connection logic. If the daemon isn't already running, then it will be
142
+ # started.
143
+ #
144
+ # The block must return nil or raise Errno::ECONNREFUSED, Errno::ENETUNREACH,
145
+ # or Errno::ETIMEDOUT to indicate that the daemon cannot be connected to.
146
+ # It must return non-nil if the daemon can be connected to.
147
+ # Upon successful connection, the return value of the block will
148
+ # be returned by #connect.
149
+ #
150
+ # Note that the block may be called multiple times.
151
+ #
152
+ # Raises:
153
+ # - StartError - an attempt to start the daemon was made, but the start
154
+ # command failed with an error.
155
+ # - StartTimeout - an attempt to start the daemon was made, but the daemon
156
+ # did not start in time, or it failed after it has gone into the background.
157
+ # - ConnectError - the daemon wasn't already running, but we couldn't connect
158
+ # to the daemon even after starting it.
159
+ def connect
160
+ connection = nil
161
+ shared_lock do
162
+ begin
163
+ connection = yield
164
+ rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::ETIMEDOUT
165
+ connection = nil
166
+ end
167
+ end
168
+ if connection.nil?
169
+ exclusive_lock do
170
+ if !daemon_is_running?
171
+ start_without_locking
172
+ end
173
+ begin
174
+ connection = yield
175
+ rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::ETIMEDOUT
176
+ connection = nil
177
+ end
178
+ if connection.nil?
179
+ # Daemon is running but we couldn't connect to it. Possible
180
+ # reasons:
181
+ # - The daemon froze.
182
+ # - Bizarre security restrictions.
183
+ # - There's a bug in the yielded code.
184
+ raise ConnectError, "Cannot connect to the daemon"
185
+ else
186
+ return connection
187
+ end
188
+ end
189
+ else
190
+ return connection
191
+ end
192
+ end
193
+
194
+ # Stop the daemon and wait until it has exited.
195
+ #
196
+ # Raises:
197
+ # - StopError - the stop command failed.
198
+ # - StopTimeout - the daemon didn't stop in time.
199
+ def stop
200
+ exclusive_lock do
201
+ begin
202
+ Timeout.timeout(@stop_timeout) do
203
+ kill_daemon
204
+ wait_until do
205
+ !daemon_is_running?
206
+ end
207
+ end
208
+ rescue Timeout::Error
209
+ raise StopTimeout, "Daemon '#{@identifier}' did not exit in time"
210
+ end
211
+ end
212
+ end
213
+
214
+ # Returns the daemon's PID, as reported by its PID file.
215
+ # This method doesn't check whether the daemon's actually running.
216
+ # Use #running? if you want to check whether it's actually running.
217
+ #
218
+ # Raises SystemCallError or IOError if something went wrong during
219
+ # reading of the PID file.
220
+ def pid
221
+ shared_lock do
222
+ return read_pid_file
223
+ end
224
+ end
225
+
226
+ # Checks whether the daemon is still running. This is done by reading
227
+ # the PID file and then checking whether there is a process with that
228
+ # PID.
229
+ #
230
+ # Raises SystemCallError or IOError if something went wrong during
231
+ # reading of the PID file.
232
+ def running?
233
+ shared_lock do
234
+ return daemon_is_running?
235
+ end
236
+ end
237
+
238
+ private
239
+ def exclusive_lock
240
+ File.open(@lock_file, 'w') do |f|
241
+ if Fcntl.const_defined? :F_SETFD
242
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
243
+ end
244
+ f.flock(File::LOCK_EX)
245
+ yield
246
+ end
247
+ end
248
+
249
+ def shared_lock
250
+ File.open(@lock_file, 'w') do |f|
251
+ if Fcntl.const_defined? :F_SETFD
252
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
253
+ end
254
+ f.flock(File::LOCK_SH)
255
+ yield
256
+ end
257
+ end
258
+
259
+ def start_without_locking
260
+ if daemon_is_running?
261
+ raise AlreadyStarted, "Daemon '#{@identifier}' is already started"
262
+ end
263
+ save_log_file_information
264
+ delete_pid_file
265
+ begin
266
+ started = false
267
+ Timeout.timeout(@start_timeout) do
268
+ done = false
269
+ spawn_daemon
270
+ record_activity
271
+
272
+ # We wait until the PID file is available and until
273
+ # the daemon responds to pings, but we wait no longer
274
+ # than @start_timeout seconds in total (including daemon
275
+ # spawn time).
276
+ # Furthermore, if the log file hasn't changed for
277
+ # @log_file_activity_timeout seconds, and the PID file
278
+ # still isn't available or the daemon still doesn't
279
+ # respond to pings, then assume that the daemon has
280
+ # terminated with an error.
281
+ wait_until do
282
+ if log_file_has_changed?
283
+ record_activity
284
+ elsif no_activity?(@log_file_activity_timeout)
285
+ raise Timeout::Error, "Daemon seems to have exited"
286
+ end
287
+ pid_file_available?
288
+ end
289
+ wait_until(@ping_interval) do
290
+ if log_file_has_changed?
291
+ record_activity
292
+ elsif no_activity?(@log_file_activity_timeout)
293
+ raise Timeout::Error, "Daemon seems to have exited"
294
+ end
295
+ run_ping_command || !daemon_is_running?
296
+ end
297
+ started = run_ping_command
298
+ end
299
+ result = started
300
+ rescue Timeout::Error
301
+ start_timed_out
302
+ if pid_file_available?
303
+ kill_daemon_with_signal
304
+ end
305
+ result = :timeout
306
+ end
307
+ if !result
308
+ raise StartError, differences_in_log_file
309
+ elsif result == :timeout
310
+ raise StartTimeout, differences_in_log_file
311
+ else
312
+ return true
313
+ end
314
+ end
315
+
316
+ def spawn_daemon
317
+ run_command(@start_command)
318
+ end
319
+
320
+ def kill_daemon
321
+ if @stop_command
322
+ begin
323
+ run_command(@stop_command)
324
+ rescue StartError => e
325
+ raise StopError, e.message
326
+ end
327
+ else
328
+ kill_daemon_with_signal
329
+ end
330
+ end
331
+
332
+ def kill_daemon_with_signal
333
+ Process.kill('SIGTERM', read_pid_file)
334
+ rescue Errno::ESRCH, Errno::ENOENT
335
+ end
336
+
337
+ def daemon_is_running?
338
+ begin
339
+ pid = read_pid_file
340
+ rescue Errno::ENOENT
341
+ # The PID file may not exist, or another thread/process
342
+ # executing #running? may have just deleted the PID file.
343
+ # So we catch this error.
344
+ pid = nil
345
+ end
346
+ if pid.nil?
347
+ return false
348
+ elsif check_pid(pid)
349
+ return true
350
+ else
351
+ delete_pid_file
352
+ return false
353
+ end
354
+ end
355
+
356
+ def read_pid_file
357
+ return File.read(@pid_file).strip.to_i
358
+ end
359
+
360
+ def delete_pid_file
361
+ File.unlink(@pid_file)
362
+ rescue Errno::EPERM, Errno::EACCES, Errno::ENOENT # ignore
363
+ end
364
+
365
+ def check_pid(pid)
366
+ Process.kill(0, pid)
367
+ return true
368
+ rescue Errno::ESRCH
369
+ return false
370
+ rescue Errno::EPERM
371
+ # We didn't have permission to kill the process. Either the process
372
+ # is owned by someone else, or the system has draconian security
373
+ # settings and we aren't allowed to kill *any* process. Assume that
374
+ # the process is running.
375
+ return true
376
+ end
377
+
378
+ def wait_until(sleep_interval = 0.1)
379
+ while !yield
380
+ sleep(sleep_interval)
381
+ end
382
+ end
383
+
384
+ def wait_until_pid_file_is_available_or_log_file_has_changed
385
+ while !(pid_file_available? || log_file_has_changed?)
386
+ sleep 0.1
387
+ end
388
+ return pid_file_is_available?
389
+ end
390
+
391
+ def wait_until_daemon_responds_to_ping_or_has_exited_or_log_file_has_changed
392
+ while !(run_ping_command || !daemon_is_running? || log_file_has_changed?)
393
+ sleep(@ping_interval)
394
+ end
395
+ return run_ping_command
396
+ end
397
+
398
+ def record_activity
399
+ @last_activity_time = Time.now
400
+ end
401
+
402
+ # Check whether there has been no recorded activity in the past +seconds+ seconds.
403
+ def no_activity?(seconds)
404
+ return Time.now - @last_activity_time > seconds
405
+ end
406
+
407
+ def pid_file_available?
408
+ return File.exist?(@pid_file) && File.stat(@pid_file).size != 0
409
+ end
410
+
411
+ # This method does nothing and only serves as a hook for the unit test.
412
+ def start_timed_out
413
+ end
414
+
415
+ def save_log_file_information
416
+ @original_log_file_stat = File.stat(@log_file) rescue nil
417
+ @current_log_file_stat = @original_log_file_stat
418
+ end
419
+
420
+ def log_file_has_changed?
421
+ if @current_log_file_stat
422
+ stat = File.stat(@log_file) rescue nil
423
+ if stat
424
+ result = @current_log_file_stat.mtime != stat.mtime ||
425
+ @current_log_file_stat.size != stat.size
426
+ @current_log_file_stat = stat
427
+ return result
428
+ else
429
+ return true
430
+ end
431
+ else
432
+ return false
433
+ end
434
+ end
435
+
436
+ def differences_in_log_file
437
+ if @original_log_file_stat
438
+ File.open(@log_file, 'r') do |f|
439
+ f.seek(@original_log_file_stat.size, IO::SEEK_SET)
440
+ return f.read.strip
441
+ end
442
+ else
443
+ return nil
444
+ end
445
+ rescue Errno::ENOENT
446
+ return nil
447
+ end
448
+
449
+ def determine_lock_file(identifier, pid_file)
450
+ return File.expand_path(pid_file + ".lock")
451
+ end
452
+
453
+ def self.fork_supported?
454
+ return RUBY_PLATFORM != "java" && RUBY_PLATFORM !~ /win32/
455
+ end
456
+
457
+ def run_command(command)
458
+ # Create tempfile for storing the command's output.
459
+ tempfile = Tempfile.new('daemon-output')
460
+ tempfile_path = tempfile.path
461
+ File.chmod(0666, tempfile_path)
462
+ tempfile.close
463
+
464
+ if self.class.fork_supported?
465
+ pid = safe_fork do
466
+ STDIN.reopen("/dev/null", "r")
467
+ STDOUT.reopen(tempfile_path, "w")
468
+ STDERR.reopen(tempfile_path, "w")
469
+ exec(command)
470
+ end
471
+ begin
472
+ Process.waitpid(pid) rescue nil
473
+ rescue Timeout::Error
474
+ # If the daemon doesn't fork into the background
475
+ # in time, then kill it.
476
+ Process.kill('SIGTERM', pid) rescue nil
477
+ begin
478
+ Timeout.timeout(5) do
479
+ Process.waitpid(pid) rescue nil
480
+ end
481
+ rescue Timeout::Error
482
+ Process.kill('SIGKILL', pid)
483
+ Process.waitpid(pid) rescue nil
484
+ end
485
+ raise
486
+ end
487
+ if $?.exitstatus != 0
488
+ raise StartError, File.read(tempfile_path).strip
489
+ end
490
+ else
491
+ if !system("#{command} >\"#{tempfile_path}\" 2>\"#{tempfile_path}\"")
492
+ raise StartError, File.read(tempfile_path).strip
493
+ end
494
+ end
495
+ ensure
496
+ File.unlink(tempfile_path) rescue nil
497
+ end
498
+
499
+ def run_ping_command
500
+ if @ping_command.respond_to?(:call)
501
+ return @ping_command.call
502
+ else
503
+ return system(@ping_command)
504
+ end
505
+ end
506
+
507
+ def safe_fork
508
+ pid = fork
509
+ if pid.nil?
510
+ begin
511
+ yield
512
+ rescue Exception => e
513
+ message = "*** Exception #{e.class} " <<
514
+ "(#{e}) (process #{$$}):\n" <<
515
+ "\tfrom " << e.backtrace.join("\n\tfrom ")
516
+ STDERR.write(e)
517
+ STDERR.flush
518
+ ensure
519
+ exit!
520
+ end
521
+ else
522
+ return pid
523
+ end
524
+ end
525
+ end
@@ -0,0 +1,300 @@
1
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ Dir.chdir(File.dirname(__FILE__))
3
+ require 'daemon_controller'
4
+ require 'benchmark'
5
+ require 'socket'
6
+
7
+ # A thread which doesn't execute its block until the
8
+ # 'go!' method has been called.
9
+ class WaitingThread < Thread
10
+ def initialize
11
+ super do
12
+ @mutex = Mutex.new
13
+ @cond = ConditionVariable.new
14
+ @go = false
15
+ @mutex.synchronize do
16
+ while !@go
17
+ @cond.wait(@mutex)
18
+ end
19
+ end
20
+ yield
21
+ end
22
+ end
23
+
24
+ def go!
25
+ @mutex.synchronize do
26
+ @go = true
27
+ @cond.broadcast
28
+ end
29
+ end
30
+ end
31
+
32
+ module TestHelpers
33
+ def new_controller(options = {})
34
+ start_command = './echo_server.rb -l echo_server.log -P echo_server.pid'
35
+ if options[:wait1]
36
+ start_command << " --wait1 #{options[:wait1]}"
37
+ end
38
+ if options[:wait2]
39
+ start_command << " --wait2 #{options[:wait2]}"
40
+ end
41
+ if options[:stop_time]
42
+ start_command << " --stop-time #{options[:stop_time]}"
43
+ end
44
+ if options[:crash_before_bind]
45
+ start_command << " --crash-before-bind"
46
+ end
47
+ new_options = {
48
+ :identifier => 'My Test Daemon',
49
+ :start_command => start_command,
50
+ :ping_command => proc do
51
+ begin
52
+ TCPSocket.new('localhost', 3230)
53
+ true
54
+ rescue SystemCallError
55
+ false
56
+ end
57
+ end,
58
+ :pid_file => 'echo_server.pid',
59
+ :log_file => 'echo_server.log',
60
+ :start_timeout => 3,
61
+ :stop_timeout => 3
62
+ }.merge(options)
63
+ @controller = DaemonController.new(new_options)
64
+ end
65
+
66
+ def write_file(filename, contents)
67
+ File.open(filename, 'w') do |f|
68
+ f.write(contents)
69
+ end
70
+ end
71
+
72
+ def exec_is_slow?
73
+ return RUBY_PLATFORM == "java"
74
+ end
75
+ end
76
+
77
+ describe DaemonController, "#start" do
78
+ before :each do
79
+ new_controller
80
+ end
81
+
82
+ include TestHelpers
83
+
84
+ it "works" do
85
+ @controller.start
86
+ @controller.stop
87
+ end
88
+
89
+ it "raises AlreadyStarted if the daemon is already running" do
90
+ @controller.should_receive(:daemon_is_running?).and_return(true)
91
+ lambda { @controller.start }.should raise_error(DaemonController::AlreadyStarted)
92
+ end
93
+
94
+ it "deletes existing PID file before starting the daemon" do
95
+ write_file('echo_server.pid', '1234')
96
+ @controller.should_receive(:daemon_is_running?).and_return(false)
97
+ @controller.should_receive(:spawn_daemon)
98
+ @controller.should_receive(:pid_file_available?).and_return(true)
99
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return(true)
100
+ @controller.start
101
+ File.exist?('echo_server.pid').should be_false
102
+ end
103
+
104
+ it "blocks until the daemon has written to its PID file" do
105
+ thread = WaitingThread.new do
106
+ sleep 0.15
107
+ write_file('echo_server.pid', '1234')
108
+ end
109
+ @controller.should_receive(:daemon_is_running?).and_return(false)
110
+ @controller.should_receive(:spawn_daemon).and_return do
111
+ thread.go!
112
+ end
113
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return(true)
114
+ begin
115
+ result = Benchmark.measure do
116
+ @controller.start
117
+ end
118
+ (0.15 .. 0.30).should === result.real
119
+ ensure
120
+ thread.join
121
+ end
122
+ end
123
+
124
+ it "blocks until the daemon can be pinged" do
125
+ ping_ok = false
126
+ running = false
127
+ thread = WaitingThread.new do
128
+ sleep 0.15
129
+ ping_ok = true
130
+ end
131
+ @controller.should_receive(:daemon_is_running?).at_least(:once).and_return do
132
+ running
133
+ end
134
+ @controller.should_receive(:spawn_daemon).and_return do
135
+ thread.go!
136
+ running = true
137
+ end
138
+ @controller.should_receive(:pid_file_available?).and_return(true)
139
+ @controller.should_receive(:run_ping_command).at_least(:once).and_return do
140
+ ping_ok
141
+ end
142
+ begin
143
+ result = Benchmark.measure do
144
+ @controller.start
145
+ end
146
+ (0.15 .. 0.30).should === result.real
147
+ ensure
148
+ thread.join
149
+ end
150
+ end
151
+
152
+ it "raises StartTimeout if the daemon doesn't start in time" do
153
+ if exec_is_slow?
154
+ start_timeout = 4
155
+ min_start_timeout = 0
156
+ max_start_timeout = 6
157
+ else
158
+ start_timeout = 0.15
159
+ max_start_timeout = 0.30
160
+ end
161
+ new_controller(:start_command => 'sleep 2', :start_timeout => start_timeout)
162
+ start_time = Time.now
163
+ end_time = nil
164
+ @controller.should_receive(:start_timed_out).and_return do
165
+ end_time = Time.now
166
+ end
167
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
168
+ (min_start_timeout .. max_start_timeout).should === end_time - start_time
169
+ end
170
+
171
+ it "kills the daemon with a signal if the daemon doesn't start in time and there's a PID file" do
172
+ new_controller(:wait2 => 3, :start_timeout => 1)
173
+ pid = nil
174
+ @controller.should_receive(:start_timed_out).and_return do
175
+ @controller.send(:wait_until) do
176
+ @controller.send(:pid_file_available?)
177
+ end
178
+ pid = @controller.send(:read_pid_file)
179
+ end
180
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
181
+ end
182
+
183
+ if DaemonController.send(:fork_supported?)
184
+ it "kills the daemon if it doesn't start in time and hasn't " <<
185
+ "forked yet, on platforms where Ruby supports fork()" do
186
+ new_controller(:start_command => '(echo $$ > echo_server.pid && sleep 5)',
187
+ :start_timeout => 0.3)
188
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
189
+ end
190
+ end
191
+
192
+ it "raises an error if the daemon exits with an error before forking" do
193
+ new_controller(:start_command => 'false')
194
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
195
+ end
196
+
197
+ it "raises an error if the daemon exits with an error after forking" do
198
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.2)
199
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
200
+ end
201
+
202
+ specify "the daemon's error output before forking is made available in the exception" do
203
+ new_controller(:start_command => '(echo hello world; false)')
204
+ begin
205
+ @controller.start
206
+ rescue DaemonController::Error => e
207
+ e.message.should == "hello world"
208
+ end
209
+ end
210
+
211
+ specify "the daemon's error output after forking is made available in the exception" do
212
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.1)
213
+ begin
214
+ @controller.start
215
+ violated
216
+ rescue DaemonController::StartTimeout => e
217
+ e.message.should =~ /crashing, as instructed/
218
+ end
219
+ end
220
+ end
221
+
222
+ describe DaemonController, "#stop" do
223
+ include TestHelpers
224
+
225
+ before :each do
226
+ new_controller
227
+ end
228
+
229
+ it "raises no exception if the daemon is not running" do
230
+ @controller.stop
231
+ end
232
+
233
+ it "waits until the daemon is no longer running" do
234
+ new_controller(:stop_time => 0.3)
235
+ @controller.start
236
+ result = Benchmark.measure do
237
+ @controller.stop
238
+ end
239
+ @controller.running?.should be_false
240
+ (0.3 .. 0.5).should === result.real
241
+ end
242
+
243
+ it "raises StopTimeout if the daemon does not stop in time" do
244
+ new_controller(:stop_time => 0.3, :stop_timeout => 0.1)
245
+ @controller.start
246
+ begin
247
+ lambda { @controller.stop }.should raise_error(DaemonController::StopTimeout)
248
+ ensure
249
+ new_controller.stop
250
+ end
251
+ end
252
+
253
+ describe "if stop command was given" do
254
+ it "raises StopError if the stop command exits with an error" do
255
+ new_controller(:stop_command => '(echo hello world; false)')
256
+ begin
257
+ begin
258
+ @controller.stop
259
+ violated
260
+ rescue DaemonController::StopError => e
261
+ e.message.should == 'hello world'
262
+ end
263
+ ensure
264
+ new_controller.stop
265
+ end
266
+ end
267
+
268
+ it "makes the stop command's error message available in the exception" do
269
+ end
270
+ end
271
+ end
272
+
273
+ describe DaemonController, "#connect" do
274
+ include TestHelpers
275
+
276
+ before :each do
277
+ new_controller
278
+ end
279
+
280
+ it "starts the daemon if it isn't already running" do
281
+ socket = @controller.connect do
282
+ TCPSocket.new('localhost', 3230)
283
+ end
284
+ socket.close
285
+ @controller.stop
286
+ end
287
+
288
+ it "connects to the existing daemon if it's already running" do
289
+ @controller.start
290
+ begin
291
+ socket = @controller.connect do
292
+ TCPSocket.new('localhost', 3230)
293
+ end
294
+ socket.close
295
+ ensure
296
+ @controller.stop
297
+ end
298
+ end
299
+ end
300
+
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ # A simple echo server, used by the unit test.
3
+ require 'socket'
4
+ require 'optparse'
5
+
6
+ options = {
7
+ :port => 3230,
8
+ :chdir => "/",
9
+ :log_file => "/dev/null",
10
+ :wait1 => 0,
11
+ :wait2 => 0,
12
+ :stop_time => 0
13
+ }
14
+ parser = OptionParser.new do |opts|
15
+ opts.banner = "Usage: echo_server.rb [options]"
16
+ opts.separator ""
17
+
18
+ opts.separator "Options:"
19
+ opts.on("-p", "--port PORT", Integer, "Port to use. Default: 3230") do |value|
20
+ options[:port] = value
21
+ end
22
+ opts.on("-C", "--change-dir DIR", String, "Change working directory. Default: /") do |value|
23
+ options[:chdir] = value
24
+ end
25
+ opts.on("-l", "--log-file FILENAME", String, "Log file to use. Default: /dev/null") do |value|
26
+ options[:log_file] = value
27
+ end
28
+ opts.on("-P", "--pid-file FILENAME", String, "Pid file to use.") do |value|
29
+ options[:pid_file] = File.expand_path(value)
30
+ end
31
+ opts.on("--wait1 SECONDS", Float, "Wait a few seconds before writing pid file.") do |value|
32
+ options[:wait1] = value
33
+ end
34
+ opts.on("--wait2 SECONDS", Float, "Wait a few seconds before binding server socket.") do |value|
35
+ options[:wait2] = value
36
+ end
37
+ opts.on("--stop-time SECONDS", Float, "Wait a few seconds before exiting.") do |value|
38
+ options[:stop_time] = value
39
+ end
40
+ opts.on("--crash-before-bind", "Whether the daemon should crash before binding the server socket.") do
41
+ options[:crash_before_bind] = true
42
+ end
43
+ end
44
+ begin
45
+ parser.parse!
46
+ rescue OptionParser::ParseError => e
47
+ puts e
48
+ puts
49
+ puts "Please see '--help' for valid options."
50
+ exit 1
51
+ end
52
+
53
+ if options[:pid_file]
54
+ if File.exist?(options[:pid_file])
55
+ STDERR.puts "*** ERROR: pid file #{options[:pid_file]} exists."
56
+ exit 1
57
+ end
58
+ end
59
+
60
+ pid = fork do
61
+ Process.setsid
62
+ fork do
63
+ STDIN.reopen("/dev/null", 'r')
64
+ STDOUT.reopen(options[:log_file], 'a')
65
+ STDERR.reopen(options[:log_file], 'a')
66
+ STDOUT.sync = true
67
+ STDERR.sync = true
68
+ Dir.chdir(options[:chdir])
69
+ File.umask(0)
70
+
71
+ if options[:pid_file]
72
+ sleep(options[:wait1])
73
+ File.open(options[:pid_file], 'w') do |f|
74
+ f.write(Process.pid)
75
+ end
76
+ at_exit do
77
+ File.unlink(options[:pid_file]) rescue nil
78
+ end
79
+ end
80
+
81
+ sleep(options[:wait2])
82
+ if options[:crash_before_bind]
83
+ puts "#{Time.now}: crashing, as instructed."
84
+ exit 2
85
+ end
86
+
87
+ server = TCPServer.new('localhost', options[:port])
88
+ begin
89
+ puts "*** #{Time.now}: echo server started"
90
+ while (client = server.accept)
91
+ puts "#{Time.now}: new client"
92
+ begin
93
+ while (line = client.readline)
94
+ puts "#{Time.now}: client sent: #{line.strip}"
95
+ client.puts(line)
96
+ end
97
+ rescue EOFError
98
+ ensure
99
+ puts "#{Time.now}: connection closed"
100
+ client.close rescue nil
101
+ end
102
+ end
103
+ rescue SignalException
104
+ exit 2
105
+ rescue => e
106
+ puts e.to_s
107
+ puts " " << e.backtrace.join("\n ")
108
+ exit 3
109
+ ensure
110
+ puts "*** #{Time.now}: echo server exiting..."
111
+ sleep(options[:stop_time])
112
+ puts "*** #{Time.now}: echo server exited"
113
+ end
114
+ end
115
+ end
116
+ Process.waitpid(pid)
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: FooBarWidget-daemon_controller
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hongli Lai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-21 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A library for implementing daemon management capabilities.
17
+ email: hongli@phusion.nl
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.rdoc
26
+ - LICENSE.txt
27
+ - daemon_controller.gemspec
28
+ - lib/daemon_controller.rb
29
+ - spec/daemon_controller_spec.rb
30
+ - spec/echo_server.rb
31
+ has_rdoc: false
32
+ homepage: http://github.com/FooBarWidget/daemon_controller/tree/master
33
+ post_install_message:
34
+ rdoc_options: []
35
+
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ version:
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.2.0
54
+ signing_key:
55
+ specification_version: 2
56
+ summary: A library for implementing daemon management capabilities
57
+ test_files: []
58
+