dante 0.1.5 → 0.3.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/test/runner_test.rb CHANGED
@@ -1,130 +1,755 @@
1
1
  require File.expand_path('../test_helper', __FILE__)
2
+ require 'socket'
2
3
 
3
4
  describe "dante runner" do
5
+
6
+ describe "verify options fails" do
7
+ it "should bubble up exception" do
8
+ runner = Dante::Runner.new('test-process', {key1:"val2"}) {
9
+ raise Exception.new("should not get here!!!")
10
+ }
11
+
12
+ runner.verify_options_hook = lambda { |opts|
13
+ raise Exception.new("Look for this exception") if(opts[:key1] != "val1")
14
+ }
15
+
16
+ err = assert_raises(Exception) {
17
+ runner.execute
18
+ }
19
+
20
+ assert_equal(err.message, "Look for this exception")
21
+ end
22
+ end
23
+
4
24
  describe "with no daemonize" do
5
25
  before do
6
- @process = TestingProcess.new('a')
26
+ @process = TestingProcess.new('a', @tmpdir)
7
27
  @runner = Dante::Runner.new('test-process') { @process.run_a! }
8
28
  @stdout = capture_stdout { @runner.execute }
9
29
  end
10
30
 
11
31
  it "prints correct stdout" do
12
- assert_match /Starting test-process/, @stdout
32
+ assert_match(/Starting test-process/, @stdout)
13
33
  end
14
34
 
15
35
  it "starts successfully when executed" do
16
36
  @output = File.read(@process.tmp_path)
17
- assert_match /Started/, @output
37
+ assert_match(/Started/, @output)
18
38
  end
19
39
  end # no daemonize
20
40
 
21
41
  describe "with daemonize flag" do
22
42
  before do
23
- @process = TestingProcess.new('b')
24
- @run_options = { :daemonize => true, :pid_path => "/tmp/dante.pid", :port => 8080, :log_path => false }
43
+ @process = TestingProcess.new('b', @tmpdir)
44
+ @pid_path = tmp_path('dante.pid')
45
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :port => 8080, :log_path => tmp_path('dante-daemon.log') }
25
46
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
26
47
  @process.run_b!(opts[:port]) }
27
48
  @stdout = capture_stdout { @runner.execute }
28
- sleep(1)
49
+ wait_for_output(@process.tmp_path)
29
50
  end
30
51
 
31
52
  it "can properly handles aborts and starts / stops on INT" do
32
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
53
+ refute_equal 0, @pid = daemon_pid(@pid_path)
33
54
  Process.kill "INT", @pid
34
- sleep(1) # Wait to complete
55
+ wait_for_exit(@pid)
35
56
  @output = File.read(@process.tmp_path)
36
57
 
37
- assert_match /Started on 8080!!/, @output
38
- assert_match /Interrupt!!/, @output
39
- assert_match /Closing!!/, @output
58
+ assert_match(/Started on 8080!!/, @output)
59
+ assert_match(/Interrupt!!/, @output)
60
+ assert_match(/Closing!!/, @output)
40
61
  end
41
62
 
42
63
  it "can properly handles aborts and starts / stops on TERM" do
43
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
64
+ refute_equal 0, @pid = daemon_pid(@pid_path)
44
65
  Process.kill "TERM", @pid
45
- sleep(1) # Wait to complete
66
+ wait_for_exit(@pid)
46
67
  @output = File.read(@process.tmp_path)
47
68
 
48
- assert_match /Started on 8080!!/, @output
49
- assert_match /Closing!!/, @output
50
- refute_match /Interrupt!!/, @output
69
+ assert_match(/Started on 8080!!/, @output)
70
+ assert_match(/Closing!!/, @output)
71
+ refute_match(/Interrupt!!/, @output)
72
+ end
73
+
74
+ it "can be stopped with #stop, which removes the pid file" do
75
+ refute_equal 0, @pid = daemon_pid(@pid_path)
76
+ @stop_output = capture_stdout { @runner.stop }
77
+
78
+ assert_match(/Daemonized process killed after term/, @stop_output)
79
+ refute File.exist?(@pid_path), "pid file left behind after stop"
80
+ assert_match(/Closing!!/, File.read(@process.tmp_path))
81
+ refute_match(/Interrupt!!/, File.read(@process.tmp_path)) # stopped with TERM, not INT
51
82
  end
52
83
  end # daemonize
53
84
 
85
+ describe "daemonize with block on sigterm" do
86
+ before do
87
+ @logfile = tmp_path('dante-logging.log')
88
+ @pid_path = tmp_path('dante.pid')
89
+ @process = TestingProcess.new('f1', @tmpdir)
90
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :log_path => @logfile }
91
+ @runner = Dante::Runner.new('test-process-f1', @run_options) { |opts|
92
+ @process.run_d! }
93
+ @stdout = capture_stdout { @runner.execute }
94
+ wait_for_output(@logfile, /The rescuer/)
95
+ end
96
+
97
+ it "cannot be stopped without force flag" do
98
+ refute_equal 0, @pid = daemon_pid(@pid_path)
99
+ @stop_output = capture_stdout { @runner.stop }
100
+ @output = File.read(@logfile)
101
+
102
+ assert_match(/Trying to stop test-process/, @output)
103
+ assert_match(/Failed to kill daemonized process/, @stop_output)
104
+ refute_match(/Daemonized process killed after kill/, @stop_output)
105
+ assert File.exist?(@pid_path), "pid file removed although the daemon is still running"
106
+ end
107
+ end
108
+
109
+ describe "daemonize with block on sigkill" do
110
+ before do
111
+ @logfile = tmp_path('dante-logging.log')
112
+ @pid_path = tmp_path('dante.pid')
113
+ @process = TestingProcess.new('f2', @tmpdir)
114
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :log_path => @logfile, force: true }
115
+ @runner = Dante::Runner.new('test-process-f2', @run_options) { |opts|
116
+ @process.run_d! }
117
+ @stdout = capture_stdout { @runner.execute }
118
+ wait_for_output(@logfile, /The rescuer/)
119
+ end
120
+
121
+ it "can be stopped without force flag" do
122
+ refute_equal 0, @pid = daemon_pid(@pid_path)
123
+ @stop_output = capture_stdout { @runner.stop }
124
+ @output = File.read(@logfile)
125
+
126
+ assert_match(/Trying to stop test-process/, @output)
127
+ assert_match(/Failed to kill daemonized process/, @stop_output)
128
+ assert_match(/Daemonized process killed after kill/, @stop_output)
129
+ refute File.exist?(@pid_path), "pid file left behind after a forced stop"
130
+ end
131
+ end
132
+
54
133
  describe "with daemonize flag and log file specified" do
55
134
  before do
56
- @logfile = '/tmp/dante-logging.log'
57
- FileUtils.rm(@logfile) if File.exist?(@logfile)
58
- @process = TestingProcess.new('c')
59
- @run_options = { :daemonize => true, :pid_path => "/tmp/dante.pid", :port => 8081, :log_path => @logfile }
135
+ @logfile = tmp_path('dante-logging.log')
136
+ @pid_path = tmp_path('dante.pid')
137
+ @process = TestingProcess.new('c', @tmpdir)
138
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :port => 8081, :log_path => @logfile }
60
139
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
61
140
  @process.run_c!(opts[:port]) }
62
- @runner.execute
63
- sleep(1)
141
+ @stdout = capture_stdout { @runner.execute }
142
+ wait_for_output(@logfile, /Started on 8081!!/)
64
143
  end
65
144
 
66
145
  it "can properly handles log to file and aborts on INT" do
67
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
146
+ refute_equal 0, @pid = daemon_pid(@pid_path)
68
147
  Process.kill "INT", @pid
69
- sleep(1) # Wait to complete
148
+ wait_for_exit(@pid)
70
149
  @output = File.read(@logfile)
71
150
 
72
- assert_match /Started on 8081!!/, @output
73
- assert_match /Interrupt!!/, @output
74
- assert_match /Closing!!/, @output
151
+ assert_match(/Started on 8081!!/, @output)
152
+ assert_match(/Interrupt!!/, @output)
153
+ assert_match(/Closing!!/, @output)
75
154
  end
76
155
  end
77
156
 
78
157
  describe "with daemonize flag and logging flag enabled, and debug flag disabled" do
79
158
  before do
80
- @logfile = '/tmp/dante-logging.log'
81
- FileUtils.rm(@logfile) if File.exist?(@logfile)
82
- @process = TestingProcess.new('c')
83
- @run_options = { :daemonize => true, :pid_path => "/tmp/dante.pid", :port => 8081, :log_path => @logfile, :debug => false }
159
+ @logfile = tmp_path('dante-logging.log')
160
+ @pid_path = tmp_path('dante.pid')
161
+ @process = TestingProcess.new('c', @tmpdir)
162
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :port => 8081, :log_path => @logfile, :debug => false }
84
163
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
85
164
  @process.run_c!(opts[:port]) }
86
- @runner.execute
87
- sleep(1)
165
+ @stdout = capture_stdout { @runner.execute }
166
+ wait_for_output(@logfile, /Started on 8081!!/)
88
167
  end
89
168
 
90
169
  it "can properly handles log to file and aborts on INT" do
91
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
170
+ refute_equal 0, @pid = daemon_pid(@pid_path)
92
171
  Process.kill "INT", @pid
93
- sleep(1) # Wait to complete
172
+ wait_for_exit(@pid)
94
173
  @output = File.read(@logfile)
95
174
 
96
- assert_match /Started on 8081!!/, @output
97
- assert_match /Interrupt received/, @output
98
- assert_match /Closing!!/, @output
175
+ assert_match(/Started on 8081!!/, @output)
176
+ assert_match(/Interrupt received/, @output)
177
+ assert_match(/Closing!!/, @output)
99
178
  end
100
179
  end
101
180
 
102
181
 
103
182
  describe "with execute accepting block" do
104
183
  before do
105
- @process = TestingProcess.new('b')
106
- @run_options = { :daemonize => true, :pid_path => "/tmp/dante.pid", :port => 8080, :log_path => false }
184
+ @process = TestingProcess.new('b', @tmpdir)
185
+ @pid_path = tmp_path('dante.pid')
186
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :port => 8080, :log_path => tmp_path('dante-daemon.log') }
107
187
  @runner = Dante::Runner.new('test-process-2', @run_options)
108
188
  @stdout = capture_stdout { @runner.execute { |opts| @process.run_b!(opts[:port]) } }
109
- sleep(1)
189
+ wait_for_output(@process.tmp_path)
110
190
  end
111
191
 
112
192
  it "can properly handles aborts and starts / stops on INT" do
113
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
193
+ refute_equal 0, @pid = daemon_pid(@pid_path)
114
194
  Process.kill "INT", @pid
115
- sleep(1) # Wait to complete
195
+ wait_for_exit(@pid)
116
196
  @output = File.read(@process.tmp_path)
117
197
 
118
- assert_match /Started on 8080!!/, @output
119
- assert_match /Interrupt!!/, @output
120
- assert_match /Closing!!/, @output
198
+ assert_match(/Started on 8080!!/, @output)
199
+ assert_match(/Interrupt!!/, @output)
200
+ assert_match(/Closing!!/, @output)
121
201
  end
122
202
  end # execute with block
123
203
 
204
+ describe "with daemonize flag, stopped while starting up" do
205
+ before do
206
+ # Holds the daemon at its "Starting" message, after its pid file is written and
207
+ # before its startup block runs, and shows that it got there by creating @starting.
208
+ # (It holds before writing the message, so the TERM below never lands mid-write.)
209
+ starting = @starting = tmp_path('starting')
210
+ slow_runner = Class.new(Dante::Runner) do
211
+ define_method(:log) do |message|
212
+ if message.start_with?("Starting")
213
+ File.write(starting, "")
214
+ sleep 10
215
+ end
216
+ super(message)
217
+ end
218
+ end
219
+ @logfile = tmp_path('dante-starting.log')
220
+ @pid_path = tmp_path('dante.pid')
221
+ @runner = slow_runner.new('test-process', :daemonize => true, :pid_path => @pid_path, :log_path => @logfile) { sleep 100 }
222
+ @stdout = capture_stdout { @runner.execute }
223
+ end
224
+
225
+ it "stops through its TERM handler once it has reported starting" do
226
+ assert_match(/Daemon has started successfully/, @stdout)
227
+ @pid = daemon_pid(@pid_path)
228
+ wait_for_output(@starting)
229
+ Process.kill "TERM", @pid
230
+ wait_for_exit(@pid)
231
+
232
+ assert_match(/Trying to stop test-process/, File.read(@logfile))
233
+ end
234
+ end # stopped while starting up
235
+
236
+ describe "with daemonize flag and a umask" do
237
+ before do
238
+ @original_umask = File.umask(0027)
239
+ @logfile = tmp_path('dante-umask.log')
240
+ @pid_path = tmp_path('dante.pid')
241
+ @created = tmp_path('created-by-daemon')
242
+ created = @created
243
+ @runner = Dante::Runner.new('test-process', :daemonize => true, :pid_path => @pid_path, :log_path => @logfile) {
244
+ File.write(created, "x")
245
+ sleep 100 }
246
+ @stdout = capture_stdout { @runner.execute }
247
+ wait_for_output(@created)
248
+ end
249
+
250
+ after do
251
+ File.umask(@original_umask)
252
+ end
253
+
254
+ it "creates files with the umask of the process that started it" do
255
+ assert_equal "640", "%o" % (File.stat(@created).mode & 0777)
256
+ end
257
+
258
+ it "still gives the log file mode 0644" do
259
+ assert_equal 0644, File.stat(@logfile).mode & 0777
260
+ end
261
+ end # umask
262
+
263
+ describe "with the standard streams connected to a socket" do
264
+ # Runs the block in a forked child whose stdin, stdout and stderr are one end of a
265
+ # socket pair, as under Spring or Zeus. Returns the child's exit status and the
266
+ # error it raised, if any. The error goes through a file rather than a pipe, which
267
+ # a daemon started by the block would hold open.
268
+ def run_with_socket_streams
269
+ error_path = tmp_path('child-error')
270
+ pid = fork do
271
+ begin
272
+ socket, _peer = UNIXSocket.pair
273
+ [STDIN, STDOUT, STDERR].each { |io| io.reopen(socket) }
274
+ $stdin, $stdout, $stderr = STDIN, STDOUT, STDERR
275
+ yield
276
+ rescue Exception => e
277
+ File.write(error_path, "#{e.class}: #{e.message}")
278
+ exit!(1)
279
+ end
280
+ exit!(0)
281
+ end
282
+ Process.wait(pid)
283
+ [$?.exitstatus, File.exist?(error_path) ? File.read(error_path) : ""]
284
+ end
285
+
286
+ def null_device?(io)
287
+ io.stat.chardev? && io.stat.rdev == File.stat(File::NULL).rdev
288
+ end
289
+
290
+ it "appends to the log file when run in the foreground" do
291
+ @logfile = tmp_path('dante-socket.log')
292
+ File.write(@logfile, "earlier line\n")
293
+ status, error = run_with_socket_streams do
294
+ Dante::Runner.new('test-process', :log_path => @logfile) { puts "Started" }.execute
295
+ end
296
+
297
+ assert_equal [0, ""], [status, error]
298
+ assert_equal "earlier line\nStarted\n", File.read(@logfile)
299
+ assert_equal 0644, File.stat(@logfile).mode & 0777
300
+ end
301
+
302
+ it "logs to the log file when daemonized" do
303
+ @logfile = tmp_path('dante-socket.log')
304
+ @pid_path = tmp_path('dante.pid')
305
+ @process = TestingProcess.new('c', @tmpdir)
306
+ status, error = run_with_socket_streams do
307
+ Dante::Runner.new('test-process', :daemonize => true, :pid_path => @pid_path, :log_path => @logfile) {
308
+ @process.run_c!(8082) }.execute
309
+ end
310
+
311
+ assert_equal [0, ""], [status, error]
312
+ wait_for_output(@logfile, /Started on 8082!!/)
313
+ assert_equal 0644, File.stat(@logfile).mode & 0777
314
+ end
315
+
316
+ it "redirects all three streams to /dev/null without a log file" do
317
+ status, error = run_with_socket_streams do
318
+ Dante::Runner.new('test-process', :log_path => false).send(:redirect_output!)
319
+ streams = [STDIN, STDOUT, STDERR]
320
+ raise "not redirected to /dev/null: #{streams.reject { |io| null_device?(io) }.map(&:fileno)}" unless streams.all? { |io| null_device?(io) }
321
+ end
322
+
323
+ assert_equal [0, ""], [status, error]
324
+ end
325
+ end # socket streams
326
+
327
+ describe "with the pid file naming a process of another user" do
328
+ before do
329
+ @pid_path = tmp_path('dante.pid')
330
+ if Process.euid == 0
331
+ # A process of root's; the blocks below run as nobody.
332
+ @foreign_pid = @sleeper = spawn('sleep', '60')
333
+ else
334
+ @foreign_pid = 1
335
+ begin
336
+ Process.kill(0, @foreign_pid)
337
+ skip "pid #{@foreign_pid} can be signalled, so there is no process of another user to name"
338
+ rescue Errno::EPERM
339
+ end
340
+ end
341
+ File.write(@pid_path, "#{@foreign_pid}\n")
342
+ File.chmod(0755, @tmpdir)
343
+ File.chmod(0644, @pid_path)
344
+ end
345
+
346
+ after do
347
+ FileUtils.rm_f(@pid_path) if @pid_path
348
+ if @sleeper
349
+ Process.kill('KILL', @sleeper)
350
+ Process.wait(@sleeper)
351
+ end
352
+ end
353
+
354
+ # Runs the block in a forked child that may not signal @foreign_pid (as nobody, when
355
+ # the tests run as root). Returns what the block returned or raised, and what it
356
+ # printed.
357
+ def run_without_permission_to_signal
358
+ out_path = tmp_path('out')
359
+ reader, writer = IO.pipe
360
+ pid = fork do
361
+ reader.close
362
+ $stdout = File.open(out_path, 'w')
363
+ $stdout.sync = true
364
+ result = begin
365
+ if Process.euid == 0
366
+ nobody = Etc.getpwnam('nobody')
367
+ Process.groups = [nobody.gid]
368
+ Process::GID.change_privilege(nobody.gid)
369
+ Process::UID.change_privilege(nobody.uid)
370
+ end
371
+ raise "still root" if Process.euid == 0
372
+ [:returned, yield]
373
+ rescue Exception => e
374
+ [:raised, "#{e.class}: #{e.message}"]
375
+ end
376
+ writer.write(Marshal.dump(result))
377
+ exit!(0)
378
+ end
379
+ writer.close
380
+ Process.wait(pid)
381
+ [Marshal.load(reader.read), File.read(out_path)]
382
+ ensure
383
+ reader.close if reader && !reader.closed?
384
+ end
385
+
386
+ it "reports the daemon as running" do
387
+ result, _output = run_without_permission_to_signal do
388
+ Dante::Runner.new('test-process', :pid_path => @pid_path).daemon_running?
389
+ end
390
+
391
+ assert_equal [:returned, true], result
392
+ end
393
+
394
+ it "does not start a second daemon" do
395
+ result, output = run_without_permission_to_signal do
396
+ Dante::Runner.new('test-process', :daemonize => true, :pid_path => @pid_path, :log_path => tmp_path('dante.log')) {
397
+ raise "should not get here" }.execute
398
+ nil
399
+ end
400
+
401
+ assert_equal [:returned, nil], result
402
+ assert_equal "Process is already started\n", output
403
+ end
404
+
405
+ it "reports that it could not stop it with -k, and keeps the pid file" do
406
+ result, output = run_without_permission_to_signal do
407
+ ARGV.replace(['-k', '-P', @pid_path])
408
+ Dante::Runner.new('test-process').execute
409
+ nil
410
+ end
411
+
412
+ assert_equal [:returned, nil], result
413
+ assert_match(/Failed to stop! : Operation not permitted/, output)
414
+ assert_match(/Failed to kill daemonized process/, output)
415
+ assert_equal "#{@foreign_pid}\n", File.read(@pid_path)
416
+ end
417
+
418
+ it "stops waiting at once when it cannot signal it, even with :force" do
419
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
420
+ result, output = run_without_permission_to_signal do
421
+ Dante::Runner.new('test-process', :pid_path => @pid_path, :force => true).stop
422
+ nil
423
+ end
424
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
425
+
426
+ assert_equal [:returned, nil], result
427
+ assert_match(/Failed to kill daemonized process/, output)
428
+ refute_match(/Sent SIG/, output)
429
+ assert_operator elapsed, :<, Dante::Runner::MAX_START_TRIES / 2.0, "waited for a daemon it could not signal"
430
+ assert_equal "#{@foreign_pid}\n", File.read(@pid_path)
431
+ end
432
+ end # another user's process
433
+
434
+ describe "with an empty pid file, as left by a crash between creating and writing it" do
435
+ before do
436
+ @pid_path = tmp_path('dante.pid')
437
+ File.write(@pid_path, "")
438
+ end
439
+
440
+ it "does not count as a running daemon, nor does a pid file naming no process" do
441
+ ["", "0\n", "-1\n"].each do |content|
442
+ File.write(@pid_path, content)
443
+ assert_equal false, Dante::Runner.new('test-process', :pid_path => @pid_path).daemon_running?, "pid file #{content.inspect}"
444
+ end
445
+ end
446
+
447
+ it "does not count a number too large for a pid as a daemon, running or to stop" do
448
+ ["2147483648\n", "99999999999999999999\n"].each do |content|
449
+ File.write(@pid_path, content)
450
+ runner = Dante::Runner.new('test-process', :pid_path => @pid_path)
451
+ assert_equal false, runner.daemon_running?, "pid file #{content.inspect}"
452
+ assert_equal "No test-process processes are running\n", capture_stdout { runner.stop }, "pid file #{content.inspect}"
453
+ end
454
+ end
455
+
456
+ it "starts a daemon in its place" do
457
+ started = tmp_path('started')
458
+ result = nil
459
+ output = capture_stdout {
460
+ result = Dante::Runner.new('test-process', :daemonize => true, :pid_path => @pid_path, :log_path => tmp_path('dante.log')) {
461
+ File.write(started, ""); sleep 100 }.execute }
462
+
463
+ assert_equal true, result
464
+ assert_match(/Daemon has started successfully/, output)
465
+ wait_for_output(started)
466
+ assert process_alive?(daemon_pid(@pid_path))
467
+ end
468
+
469
+ it "reports that no daemon is running when asked to stop, and signals nothing" do
470
+ # The stop runs in a child leading a process group of its own, which is all that a
471
+ # signal to "pid 0" (the caller's process group) could reach.
472
+ out_path = tmp_path('out')
473
+ pid = fork do
474
+ Process.setpgid(0, 0)
475
+ $stdout = File.open(out_path, 'w')
476
+ $stdout.sync = true
477
+ p Dante::Runner.new('test-process', :pid_path => @pid_path).stop
478
+ exit!(0)
479
+ end
480
+ Process.wait(pid)
481
+
482
+ assert_equal "No test-process processes are running\nfalse\n", File.read(out_path)
483
+ assert_equal 0, $?.exitstatus, "the stop ended with #{$?.inspect}"
484
+ assert_equal "", File.read(@pid_path)
485
+ end
486
+ end # empty pid file
487
+
488
+ describe "with daemonize flag, controlling a running daemon" do
489
+ before do
490
+ @pid_path = tmp_path('dante.pid')
491
+ @logfile = tmp_path('dante.log')
492
+ @first_started = tmp_path('first-started')
493
+ @run_options = { :daemonize => true, :pid_path => @pid_path, :log_path => @logfile }
494
+ first_started = @first_started
495
+ @start_result = nil
496
+ @stdout = capture_stdout {
497
+ @start_result = Dante::Runner.new('test-process', @run_options) { File.write(first_started, ""); sleep 100 }.execute }
498
+ wait_for_output(@first_started)
499
+ @pid = daemon_pid(@pid_path)
500
+ end
501
+
502
+ it "runs the daemon in a session of its own, which it does not lead" do
503
+ assert_equal true, @start_result
504
+ refute_equal Process.getsid, Process.getsid(@pid)
505
+ refute_equal @pid, Process.getsid(@pid)
506
+ end
507
+
508
+ it "does not start a second daemon on the same pid file" do
509
+ second_started = tmp_path('second-started')
510
+ result = :not_returned
511
+ output = capture_stdout {
512
+ result = Dante::Runner.new('test-process', @run_options) { File.write(second_started, "") }.execute }
513
+
514
+ assert_equal ["Process is already started\n"], output.lines.grep_v(/: warning: /) # Ruby 2.7 -w warns on stderr
515
+ assert_nil result
516
+ assert_equal "#{@pid}\n", File.read(@pid_path)
517
+ assert process_alive?(@pid)
518
+ end
519
+
520
+ it "stops the daemon and starts a new one with :restart" do
521
+ second_started = tmp_path('second-started')
522
+ result = nil
523
+ output = capture_stdout {
524
+ result = Dante::Runner.new('test-process', @run_options.merge(:restart => true)) {
525
+ File.write(second_started, ""); sleep 100 }.execute }
526
+
527
+ assert_equal true, result
528
+ assert_match(/Daemonized process killed after term.*Daemon has started successfully/m, output)
529
+ refute process_alive?(@pid), "the first daemon is still running"
530
+ wait_for_output(second_started)
531
+ new_pid = daemon_pid(@pid_path)
532
+ refute_equal @pid, new_pid
533
+ assert process_alive?(new_pid)
534
+ end
535
+
536
+ it "stops the daemon with -k and removes its pid file" do
537
+ ARGV.replace(['-k', '-P', @pid_path])
538
+ output = capture_stdout { Dante::Runner.new('test-process').execute }
539
+
540
+ assert_match(/Sent SIGTERM to PID: #{@pid}/, output)
541
+ assert_match(/Daemonized process killed after term/, output)
542
+ refute process_alive?(@pid), "daemon still running after -k"
543
+ refute File.exist?(@pid_path), "pid file left behind after -k"
544
+ end
545
+ end # controlling a running daemon
546
+
547
+ describe "with daemonize flag and no log path" do
548
+ before do
549
+ # Records the log path the daemon would redirect its output to, then sends the
550
+ # output to a file of this test's instead of writing to /var/log.
551
+ recorded = @recorded = tmp_path('recorded-log-path')
552
+ logfile = tmp_path('dante.log')
553
+ recording_runner = Class.new(Dante::Runner) do
554
+ define_method(:redirect_output!) do
555
+ File.write("#{recorded}.tmp", options[:log_path].inspect)
556
+ File.rename("#{recorded}.tmp", recorded)
557
+ options[:log_path] = logfile
558
+ super()
559
+ end
560
+ end
561
+ @pid_path = tmp_path('dante.pid')
562
+ @stdout = capture_stdout {
563
+ recording_runner.new('test-process', :daemonize => true, :pid_path => @pid_path) { sleep 100 }.execute }
564
+ daemon_pid(@pid_path)
565
+ wait_for_output(@recorded)
566
+ end
567
+
568
+ it "logs to /var/log/<name>.log" do
569
+ assert_equal '"/var/log/test-process.log"', File.read(@recorded)
570
+ end
571
+ end # default log path
572
+
573
+ describe "with daemonize flag, when the daemon dies before recording its pid" do
574
+ it "reports that it could not be started" do
575
+ # The daemon fails to create the pid file's directory, which is a device.
576
+ pid_path = File.join(File::NULL, 'dante.pid')
577
+ result = nil
578
+ output = capture_stdout {
579
+ result = Dante::Runner.new('test-process', :daemonize => true, :pid_path => pid_path,
580
+ :log_path => tmp_path('dante.log')) { raise "should not get here" }.execute }
581
+
582
+ assert_equal false, result
583
+ assert_match(/Daemonized process couldn't be started/, output)
584
+ refute_match(/started successfully/, output)
585
+ end
586
+ end # dies at once
587
+
588
+ describe "with daemonize flag, when the daemon dies right after recording its pid" do
589
+ it "reports that it could not be started" do
590
+ pid_path = tmp_path('dante.pid')
591
+ # After the check made before starting, checks for the daemon only once its pid is
592
+ # recorded, so the check falls in the moment between the daemon starting up and dying.
593
+ checking_runner = Class.new(Dante::Runner) do
594
+ define_method(:daemon_running?) do
595
+ if @checked_before_starting
596
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10
597
+ sleep 0.01 until (File.exist?(pid_path) && File.read(pid_path).to_i > 0) ||
598
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
599
+ end
600
+ @checked_before_starting = true
601
+ super()
602
+ end
603
+ end
604
+ result = nil
605
+ output = capture_stdout {
606
+ result = checking_runner.new('test-process', :daemonize => true, :pid_path => pid_path,
607
+ :log_path => tmp_path('dante.log')) { sleep 0.2; raise "boom" }.execute }
608
+
609
+ assert_equal false, result
610
+ assert_match(/Daemonized process couldn't be started/, output)
611
+ refute_match(/started successfully/, output)
612
+ assert_match(/Starting test-process/, File.read(tmp_path('dante.log'))) # it did start, then died
613
+ end
614
+
615
+ it "removes the pid file the daemon left" do
616
+ pid_path = tmp_path('dante.pid')
617
+ result = nil
618
+ capture_stdout {
619
+ result = Dante::Runner.new('test-process', :daemonize => true, :pid_path => pid_path,
620
+ :log_path => tmp_path('dante.log')) { raise "boom" }.execute }
621
+
622
+ assert_equal false, result
623
+ assert_match(/Starting test-process/, File.read(tmp_path('dante.log'))) # it recorded its pid and started
624
+ refute File.exist?(pid_path), "the dead daemon's pid file was left behind"
625
+ end
626
+
627
+ it "keeps the pid file when it names another process" do
628
+ pid_path = tmp_path('dante.pid')
629
+ other = spawn('sleep', '30')
630
+ # The daemon records another process's pid, as if another start had replaced the
631
+ # pid file after this daemon wrote it.
632
+ other_runner = Class.new(Dante::Runner) do
633
+ define_method(:store_pid) { |_pid| super(other) }
634
+ end
635
+ result = nil
636
+ capture_stdout {
637
+ result = other_runner.new('test-process', :daemonize => true, :pid_path => pid_path,
638
+ :log_path => tmp_path('dante.log')) { raise "boom" }.execute }
639
+
640
+ assert_equal false, result
641
+ assert_equal "#{other}\n", File.read(pid_path)
642
+ ensure
643
+ if other
644
+ Process.kill('KILL', other)
645
+ Process.wait(other)
646
+ end
647
+ end
648
+ end # dies after recording its pid
649
+
650
+ describe "with daemonize flag, interrupted while the daemon starts up" do
651
+ it "leaves no zombie behind" do
652
+ pid_path = tmp_path('dante.pid')
653
+ leader_path = tmp_path('leader')
654
+ leader = nil
655
+ # The daemon records its parent, the session leader, which then waits out the
656
+ # grace period while the caller waits for it; that wait is interrupted.
657
+ interrupter = Thread.new(Thread.current) do |caller|
658
+ leader = wait_until { File.exist?(leader_path) && (n = File.read(leader_path).to_i) > 0 && n }
659
+ caller.raise(Interrupt) if leader
660
+ end
661
+ capture_stdout {
662
+ assert_raises(Interrupt) {
663
+ Dante::Runner.new('test-process', :daemonize => true, :pid_path => pid_path, :log_path => tmp_path('dante.log')) {
664
+ File.write("#{leader_path}.tmp", Process.ppid.to_s)
665
+ File.rename("#{leader_path}.tmp", leader_path)
666
+ sleep 100 }.execute } }
667
+ interrupter.join
668
+ assert leader, "the daemon never recorded its parent"
669
+
670
+ # A zombie still answers signal 0; a reaped process does not.
671
+ gone = wait_until(10) do
672
+ begin
673
+ Process.kill(0, leader)
674
+ false
675
+ rescue Errno::ESRCH
676
+ true
677
+ end
678
+ end
679
+ assert gone, "the session leader #{leader} was left as a zombie"
680
+ ensure
681
+ begin
682
+ Process.waitpid(leader) if leader
683
+ rescue Errno::ECHILD
684
+ end
685
+ end
686
+ end # interrupted while starting
687
+
688
+ describe "with daemonize flag, in a program of its own" do
689
+ # Runs script with dante loaded in a separate Ruby process, passing it args, and
690
+ # returns what it printed, without Ruby 2.7's -w warnings. A process of its own,
691
+ # because the test harness keeps forked processes from running at_exit handlers.
692
+ # It runs outside Bundler's environment: it needs only dante, and bundler/setup
693
+ # would print git's errors where the tree is not a git checkout.
694
+ def run_program(script, *args)
695
+ lib = File.expand_path('../../lib', __FILE__)
696
+ run = lambda { IO.popen([RbConfig.ruby, '-I', lib, '-rdante', '-e', script, *args], :err => [:child, :out], &:read) }
697
+ output = defined?(Bundler) ? Bundler.with_unbundled_env(&run) : run.call
698
+ output.lines.grep_v(/: warning: /).join
699
+ end
700
+
701
+ before do
702
+ @pid_path = tmp_path('dante.pid')
703
+ @logfile = tmp_path('dante.log')
704
+ end
705
+
706
+ it "reports a running daemon as started whatever the program's at_exit handlers do" do
707
+ output = run_program(<<-RUBY, @pid_path, @logfile)
708
+ main = Process.pid
709
+ at_exit { exit 1 if Process.pid != main }
710
+ p Dante::Runner.new('test-process', :daemonize => true, :pid_path => ARGV[0], :log_path => ARGV[1]) { sleep 30 }.execute
711
+ RUBY
712
+
713
+ assert_equal "Daemon has started successfully\ntrue\n", output
714
+ pid = daemon_pid(@pid_path)
715
+ Process.kill('TERM', pid)
716
+ wait_for_exit(pid)
717
+ end
718
+
719
+ it "reports whether the daemon started when the program ignores SIGCHLD" do
720
+ script = <<-RUBY
721
+ trap('CHLD', 'IGNORE')
722
+ p Dante::Runner.new('test-process', :daemonize => true, :pid_path => ARGV[0], :log_path => ARGV[1]) {
723
+ raise "boom" if ARGV[2] == 'crash'
724
+ sleep 30 }.execute
725
+ RUBY
726
+
727
+ assert_equal "Daemon has started successfully\ntrue\n", run_program(script, @pid_path, @logfile, 'run')
728
+ pid = daemon_pid(@pid_path)
729
+ Process.kill('TERM', pid)
730
+ wait_for_exit(pid)
731
+
732
+ crash_pid_path = tmp_path('crash.pid')
733
+ assert_equal "Daemonized process couldn't be started\nfalse\n", run_program(script, crash_pid_path, @logfile, 'crash')
734
+ refute File.exist?(crash_pid_path), "the dead daemon's pid file was left behind"
735
+ end
736
+ end # program of its own
737
+
738
+ describe "with the -p option" do
739
+ it "passes the port to the startup block as an Integer" do
740
+ ARGV.replace(['-p', '1234'])
741
+ port = nil
742
+ capture_stdout { Dante::Runner.new('test-process').execute { |opts| port = opts[:port] } }
743
+
744
+ assert_equal 1234, port
745
+ assert_kind_of Integer, port
746
+ end
747
+ end # -p
748
+
124
749
  describe "with parsing options" do
125
750
  before do
126
- Object.send(:remove_const, 'ARGV'); ARGV = ['-t test_text']
127
- @process = TestingProcess.new('a')
751
+ ARGV.replace(['-t test_text'])
752
+ @process = TestingProcess.new('a', @tmpdir)
128
753
  @runner = Dante::Runner.new('test-process')
129
754
  @runner.with_options do |opts|
130
755
  opts.on("-t", "--test TEST", String, "Test this thing") { |test| options[:test] = test }
@@ -133,32 +758,33 @@ describe "dante runner" do
133
758
  end
134
759
 
135
760
  it "prints correct stdout" do
136
- assert_match /Starting test-process/, @stdout
761
+ assert_match(/Starting test-process/, @stdout)
137
762
  end
138
763
 
139
764
  it "prints correct data" do
140
765
  @output = File.read(@process.tmp_path)
141
- assert_match /test_text/, @output
766
+ assert_match(/test_text/, @output)
142
767
  end
143
768
 
144
769
  it "starts successfully when executed" do
145
770
  @output = File.read(@process.tmp_path)
146
- assert_match /Started/, @output
771
+ assert_match(/Started/, @output)
147
772
  end
148
773
  end # options parsing
149
774
 
150
775
  describe "with help command" do
151
776
  before do
152
- Object.send(:remove_const, 'ARGV'); ARGV = ['--help']
153
- @process = TestingProcess.new('a')
777
+ ARGV.replace(['--help'])
778
+ @process = TestingProcess.new('a', @tmpdir)
154
779
  @runner = Dante::Runner.new('test-process')
155
780
  @runner.description = "Test process banana"
156
- @runner.expects(:exit).once.returns(true)
157
- @stdout = capture_stdout { @runner.execute { @process.run_a! } }
781
+ @stdout = capture_stdout {
782
+ assert_raises(SystemExit) { @runner.execute { @process.run_a! } }
783
+ }
158
784
  end
159
785
 
160
786
  it "prints correct stdout" do
161
- assert_match /test-process - Test process banana/, @stdout
787
+ assert_match(/test-process - Test process banana/, @stdout)
162
788
  end
163
789
  end # help
164
- end
790
+ end