secobarbital-daemon_controller 0.2.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.
@@ -0,0 +1,77 @@
1
+ # daemon_controller, library for robust daemon management
2
+ # Copyright (c) 2008 Phusion
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ class DaemonController
23
+ class LockFile
24
+ class AlreadyLocked < StandardError
25
+ end
26
+
27
+ def initialize(filename)
28
+ @filename = filename
29
+ end
30
+
31
+ def exclusive_lock
32
+ File.open(@filename, 'w') do |f|
33
+ if Fcntl.const_defined? :F_SETFD
34
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
35
+ end
36
+ f.flock(File::LOCK_EX)
37
+ yield
38
+ end
39
+ end
40
+
41
+ def shared_lock
42
+ File.open(@filename, 'w') do |f|
43
+ if Fcntl.const_defined? :F_SETFD
44
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
45
+ end
46
+ f.flock(File::LOCK_SH)
47
+ yield
48
+ end
49
+ end
50
+
51
+ def try_shared_lock
52
+ File.open(@filename, 'w') do |f|
53
+ if Fcntl.const_defined? :F_SETFD
54
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
55
+ end
56
+ if f.flock(File::LOCK_SH | File::LOCK_NB)
57
+ yield
58
+ else
59
+ raise AlreadyLocked
60
+ end
61
+ end
62
+ end
63
+
64
+ def try_exclusive_lock
65
+ File.open(@filename, 'w') do |f|
66
+ if Fcntl.const_defined? :F_SETFD
67
+ f.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
68
+ end
69
+ if f.flock(File::LOCK_EX | File::LOCK_NB)
70
+ yield
71
+ else
72
+ raise AlreadyLocked
73
+ end
74
+ end
75
+ end
76
+ end # class LockFile
77
+ end # class DaemonController
@@ -0,0 +1,355 @@
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
+ min_start_timeout = 0.15
160
+ max_start_timeout = 0.30
161
+ end
162
+ new_controller(:start_command => 'sleep 2', :start_timeout => start_timeout)
163
+ start_time = Time.now
164
+ end_time = nil
165
+ @controller.should_receive(:start_timed_out).and_return do
166
+ end_time = Time.now
167
+ end
168
+ begin
169
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
170
+ (min_start_timeout .. max_start_timeout).should === end_time - start_time
171
+ ensure
172
+ @controller.stop
173
+ end
174
+ end
175
+
176
+ it "kills the daemon with a signal if the daemon doesn't start in time and there's a PID file" do
177
+ new_controller(:wait2 => 3, :start_timeout => 1)
178
+ pid = nil
179
+ @controller.should_receive(:start_timed_out).and_return do
180
+ @controller.send(:wait_until) do
181
+ @controller.send(:pid_file_available?)
182
+ end
183
+ pid = @controller.send(:read_pid_file)
184
+ end
185
+ begin
186
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
187
+ ensure
188
+ # It's possible that because of a racing condition, the PID
189
+ # file doesn't get deleted before the next test is run. So
190
+ # here we ensure that the PID file is gone.
191
+ File.unlink("echo_server.pid") rescue nil
192
+ end
193
+ end
194
+
195
+ if DaemonController.send(:fork_supported?)
196
+ it "kills the daemon if it doesn't start in time and hasn't " <<
197
+ "forked yet, on platforms where Ruby supports fork()" do
198
+ new_controller(:start_command => '(echo $$ > echo_server.pid && sleep 5)',
199
+ :start_timeout => 0.3)
200
+ lambda { @controller.start }.should raise_error(DaemonController::StartTimeout)
201
+ end
202
+ end
203
+
204
+ it "raises an error if the daemon exits with an error before forking" do
205
+ new_controller(:start_command => 'false')
206
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
207
+ end
208
+
209
+ it "raises an error if the daemon exits with an error after forking" do
210
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.2)
211
+ lambda { @controller.start }.should raise_error(DaemonController::Error)
212
+ end
213
+
214
+ specify "the daemon's error output before forking is made available in the exception" do
215
+ new_controller(:start_command => '(echo hello world; false)')
216
+ begin
217
+ @controller.start
218
+ rescue DaemonController::Error => e
219
+ e.message.should == "hello world"
220
+ end
221
+ end
222
+
223
+ specify "the daemon's error output after forking is made available in the exception" do
224
+ new_controller(:crash_before_bind => true, :log_file_activity_timeout => 0.1)
225
+ begin
226
+ @controller.start
227
+ violated
228
+ rescue DaemonController::StartTimeout => e
229
+ e.message.should =~ /crashing, as instructed/
230
+ end
231
+ end
232
+ end
233
+
234
+ describe DaemonController, "#stop" do
235
+ include TestHelpers
236
+
237
+ before :each do
238
+ new_controller
239
+ end
240
+
241
+ it "raises no exception if the daemon is not running" do
242
+ @controller.stop
243
+ end
244
+
245
+ it "waits until the daemon is no longer running" do
246
+ new_controller(:stop_time => 0.3)
247
+ @controller.start
248
+ result = Benchmark.measure do
249
+ @controller.stop
250
+ end
251
+ @controller.running?.should be_false
252
+ (0.3 .. 0.5).should === result.real
253
+ end
254
+
255
+ it "raises StopTimeout if the daemon does not stop in time" do
256
+ new_controller(:stop_time => 0.3, :stop_timeout => 0.1)
257
+ @controller.start
258
+ begin
259
+ lambda { @controller.stop }.should raise_error(DaemonController::StopTimeout)
260
+ ensure
261
+ new_controller.stop
262
+ end
263
+ end
264
+
265
+ describe "if stop command was given" do
266
+ it "raises StopError if the stop command exits with an error" do
267
+ new_controller(:stop_command => '(echo hello world; false)')
268
+ begin
269
+ begin
270
+ @controller.stop
271
+ violated
272
+ rescue DaemonController::StopError => e
273
+ e.message.should == 'hello world'
274
+ end
275
+ ensure
276
+ new_controller.stop
277
+ end
278
+ end
279
+
280
+ it "makes the stop command's error message available in the exception" do
281
+ end
282
+ end
283
+ end
284
+
285
+ describe DaemonController, "#connect" do
286
+ include TestHelpers
287
+
288
+ before :each do
289
+ new_controller
290
+ end
291
+
292
+ it "starts the daemon if it isn't already running" do
293
+ socket = @controller.connect do
294
+ TCPSocket.new('localhost', 3230)
295
+ end
296
+ socket.close
297
+ @controller.stop
298
+ end
299
+
300
+ it "connects to the existing daemon if it's already running" do
301
+ @controller.start
302
+ begin
303
+ socket = @controller.connect do
304
+ TCPSocket.new('localhost', 3230)
305
+ end
306
+ socket.close
307
+ ensure
308
+ @controller.stop
309
+ end
310
+ end
311
+ end
312
+
313
+ describe DaemonController do
314
+ include TestHelpers
315
+
316
+ specify "if the ping command is a block that raises Errno::ECONNREFUSED, then that's " <<
317
+ "an indication that the daemon cannot be connected to" do
318
+ new_controller(:ping_command => lambda do
319
+ raise Errno::ECONNREFUSED, "dummy"
320
+ end)
321
+ @controller.send(:run_ping_command).should be_false
322
+ end
323
+
324
+ specify "if the ping command is a block that returns an object that responds to #close, " <<
325
+ "then the close method will be called on that object" do
326
+ server = TCPServer.new('localhost', 8278)
327
+ begin
328
+ socket = nil
329
+ new_controller(:ping_command => lambda do
330
+ socket = TCPSocket.new('localhost', 8278)
331
+ end)
332
+ @controller.send(:run_ping_command)
333
+ socket.should be_closed
334
+ ensure
335
+ server.close
336
+ end
337
+ end
338
+
339
+ specify "if the ping command is a block that returns an object that responds to #close, " <<
340
+ "and #close raises an exception, then that exception is ignored" do
341
+ server = TCPServer.new('localhost', 8278)
342
+ begin
343
+ o = Object.new
344
+ o.should_receive(:close).and_return do
345
+ raise StandardError, "foo"
346
+ end
347
+ new_controller(:ping_command => lambda do
348
+ o
349
+ end)
350
+ lambda { @controller.send(:run_ping_command) }.should_not raise_error(StandardError)
351
+ ensure
352
+ server.close
353
+ end
354
+ end
355
+ end
@@ -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)