dante 0.2.0 → 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,4 +1,5 @@
1
1
  require File.expand_path('../test_helper', __FILE__)
2
+ require 'socket'
2
3
 
3
4
  describe "dante runner" do
4
5
 
@@ -22,128 +23,733 @@ describe "dante runner" do
22
23
 
23
24
  describe "with no daemonize" do
24
25
  before do
25
- @process = TestingProcess.new('a')
26
+ @process = TestingProcess.new('a', @tmpdir)
26
27
  @runner = Dante::Runner.new('test-process') { @process.run_a! }
27
28
  @stdout = capture_stdout { @runner.execute }
28
29
  end
29
30
 
30
31
  it "prints correct stdout" do
31
- assert_match /Starting test-process/, @stdout
32
+ assert_match(/Starting test-process/, @stdout)
32
33
  end
33
34
 
34
35
  it "starts successfully when executed" do
35
36
  @output = File.read(@process.tmp_path)
36
- assert_match /Started/, @output
37
+ assert_match(/Started/, @output)
37
38
  end
38
39
  end # no daemonize
39
40
 
40
41
  describe "with daemonize flag" do
41
42
  before do
42
- @process = TestingProcess.new('b')
43
- @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') }
44
46
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
45
47
  @process.run_b!(opts[:port]) }
46
48
  @stdout = capture_stdout { @runner.execute }
47
- sleep(1)
49
+ wait_for_output(@process.tmp_path)
48
50
  end
49
51
 
50
52
  it "can properly handles aborts and starts / stops on INT" do
51
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
53
+ refute_equal 0, @pid = daemon_pid(@pid_path)
52
54
  Process.kill "INT", @pid
53
- sleep(1) # Wait to complete
55
+ wait_for_exit(@pid)
54
56
  @output = File.read(@process.tmp_path)
55
57
 
56
- assert_match /Started on 8080!!/, @output
57
- assert_match /Interrupt!!/, @output
58
- assert_match /Closing!!/, @output
58
+ assert_match(/Started on 8080!!/, @output)
59
+ assert_match(/Interrupt!!/, @output)
60
+ assert_match(/Closing!!/, @output)
59
61
  end
60
62
 
61
63
  it "can properly handles aborts and starts / stops on TERM" do
62
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
64
+ refute_equal 0, @pid = daemon_pid(@pid_path)
63
65
  Process.kill "TERM", @pid
64
- sleep(1) # Wait to complete
66
+ wait_for_exit(@pid)
65
67
  @output = File.read(@process.tmp_path)
66
68
 
67
- assert_match /Started on 8080!!/, @output
68
- assert_match /Closing!!/, @output
69
- 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
70
82
  end
71
83
  end # daemonize
72
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
+
73
133
  describe "with daemonize flag and log file specified" do
74
134
  before do
75
- @logfile = '/tmp/dante-logging.log'
76
- FileUtils.rm(@logfile) if File.exist?(@logfile)
77
- @process = TestingProcess.new('c')
78
- @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 }
79
139
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
80
140
  @process.run_c!(opts[:port]) }
81
- @runner.execute
82
- sleep(1)
141
+ @stdout = capture_stdout { @runner.execute }
142
+ wait_for_output(@logfile, /Started on 8081!!/)
83
143
  end
84
144
 
85
145
  it "can properly handles log to file and aborts on INT" do
86
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
146
+ refute_equal 0, @pid = daemon_pid(@pid_path)
87
147
  Process.kill "INT", @pid
88
- sleep(1) # Wait to complete
148
+ wait_for_exit(@pid)
89
149
  @output = File.read(@logfile)
90
150
 
91
- assert_match /Started on 8081!!/, @output
92
- assert_match /Interrupt!!/, @output
93
- assert_match /Closing!!/, @output
151
+ assert_match(/Started on 8081!!/, @output)
152
+ assert_match(/Interrupt!!/, @output)
153
+ assert_match(/Closing!!/, @output)
94
154
  end
95
155
  end
96
156
 
97
157
  describe "with daemonize flag and logging flag enabled, and debug flag disabled" do
98
158
  before do
99
- @logfile = '/tmp/dante-logging.log'
100
- FileUtils.rm(@logfile) if File.exist?(@logfile)
101
- @process = TestingProcess.new('c')
102
- @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 }
103
163
  @runner = Dante::Runner.new('test-process-2', @run_options) { |opts|
104
164
  @process.run_c!(opts[:port]) }
105
- @runner.execute
106
- sleep(1)
165
+ @stdout = capture_stdout { @runner.execute }
166
+ wait_for_output(@logfile, /Started on 8081!!/)
107
167
  end
108
168
 
109
169
  it "can properly handles log to file and aborts on INT" do
110
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
170
+ refute_equal 0, @pid = daemon_pid(@pid_path)
111
171
  Process.kill "INT", @pid
112
- sleep(1) # Wait to complete
172
+ wait_for_exit(@pid)
113
173
  @output = File.read(@logfile)
114
174
 
115
- assert_match /Started on 8081!!/, @output
116
- assert_match /Interrupt received/, @output
117
- assert_match /Closing!!/, @output
175
+ assert_match(/Started on 8081!!/, @output)
176
+ assert_match(/Interrupt received/, @output)
177
+ assert_match(/Closing!!/, @output)
118
178
  end
119
179
  end
120
180
 
121
181
 
122
182
  describe "with execute accepting block" do
123
183
  before do
124
- @process = TestingProcess.new('b')
125
- @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') }
126
187
  @runner = Dante::Runner.new('test-process-2', @run_options)
127
188
  @stdout = capture_stdout { @runner.execute { |opts| @process.run_b!(opts[:port]) } }
128
- sleep(1)
189
+ wait_for_output(@process.tmp_path)
129
190
  end
130
191
 
131
192
  it "can properly handles aborts and starts / stops on INT" do
132
- refute_equal 0, @pid = `cat /tmp/dante.pid`.to_i
193
+ refute_equal 0, @pid = daemon_pid(@pid_path)
133
194
  Process.kill "INT", @pid
134
- sleep(1) # Wait to complete
195
+ wait_for_exit(@pid)
135
196
  @output = File.read(@process.tmp_path)
136
197
 
137
- assert_match /Started on 8080!!/, @output
138
- assert_match /Interrupt!!/, @output
139
- assert_match /Closing!!/, @output
198
+ assert_match(/Started on 8080!!/, @output)
199
+ assert_match(/Interrupt!!/, @output)
200
+ assert_match(/Closing!!/, @output)
140
201
  end
141
202
  end # execute with block
142
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
+
143
749
  describe "with parsing options" do
144
750
  before do
145
- Object.send(:remove_const, 'ARGV'); ARGV = ['-t test_text']
146
- @process = TestingProcess.new('a')
751
+ ARGV.replace(['-t test_text'])
752
+ @process = TestingProcess.new('a', @tmpdir)
147
753
  @runner = Dante::Runner.new('test-process')
148
754
  @runner.with_options do |opts|
149
755
  opts.on("-t", "--test TEST", String, "Test this thing") { |test| options[:test] = test }
@@ -152,32 +758,33 @@ describe "dante runner" do
152
758
  end
153
759
 
154
760
  it "prints correct stdout" do
155
- assert_match /Starting test-process/, @stdout
761
+ assert_match(/Starting test-process/, @stdout)
156
762
  end
157
763
 
158
764
  it "prints correct data" do
159
765
  @output = File.read(@process.tmp_path)
160
- assert_match /test_text/, @output
766
+ assert_match(/test_text/, @output)
161
767
  end
162
768
 
163
769
  it "starts successfully when executed" do
164
770
  @output = File.read(@process.tmp_path)
165
- assert_match /Started/, @output
771
+ assert_match(/Started/, @output)
166
772
  end
167
773
  end # options parsing
168
774
 
169
775
  describe "with help command" do
170
776
  before do
171
- Object.send(:remove_const, 'ARGV'); ARGV = ['--help']
172
- @process = TestingProcess.new('a')
777
+ ARGV.replace(['--help'])
778
+ @process = TestingProcess.new('a', @tmpdir)
173
779
  @runner = Dante::Runner.new('test-process')
174
780
  @runner.description = "Test process banana"
175
- @runner.expects(:exit).once.returns(true)
176
- @stdout = capture_stdout { @runner.execute { @process.run_a! } }
781
+ @stdout = capture_stdout {
782
+ assert_raises(SystemExit) { @runner.execute { @process.run_a! } }
783
+ }
177
784
  end
178
785
 
179
786
  it "prints correct stdout" do
180
- assert_match /test-process - Test process banana/, @stdout
787
+ assert_match(/test-process - Test process banana/, @stdout)
181
788
  end
182
789
  end # help
183
- end
790
+ end