sd_listen_fds emulation cleanup
[unicorn.git] / test / exec / test_exec.rb
blobaf6f15130c7bc72273069b04fb3ee6be2f3ba6f0
1 # -*- encoding: binary -*-
3 # Copyright (c) 2009 Eric Wong
4 FLOCK_PATH = File.expand_path(__FILE__)
5 require './test/test_helper'
7 do_test = true
8 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
9 redirect_test_io do
10   do_test = system($unicorn_bin, '-v')
11 end
13 unless do_test
14   warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
15        "skipping this test"
16 end
18 unless try_require('rack')
19   warn "Unable to load Rack, skipping this test"
20   do_test = false
21 end
23 class ExecTest < Test::Unit::TestCase
24   trap(:QUIT, 'IGNORE')
26   HI = <<-EOS
27 use Rack::ContentLength
28 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
29   EOS
31   SHOW_RACK_ENV = <<-EOS
32 use Rack::ContentLength
33 run proc { |env|
34   [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
36   EOS
38   HELLO = <<-EOS
39 class Hello
40   def call(env)
41     [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
42   end
43 end
44   EOS
46   COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
48   HEAVY_CFG = <<-EOS
49 worker_processes 4
50 timeout 30
51 logger Logger.new('#{COMMON_TMP.path}')
52 before_fork do |server, worker|
53   server.logger.info "before_fork: worker=\#{worker.nr}"
54 end
55   EOS
57   WORKING_DIRECTORY_CHECK_RU = <<-EOS
58 use Rack::ContentLength
59 run lambda { |env|
60   pwd = ENV['PWD']
61   a = ::File.stat(pwd)
62   b = ::File.stat(Dir.pwd)
63   if (a.ino == b.ino && a.dev == b.dev)
64     [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ]
65   else
66     [ 404, { 'Content-Type' => 'text/plain' }, [] ]
67   end
69   EOS
71   def setup
72     @pwd = Dir.pwd
73     @tmpfile = Tempfile.new('unicorn_exec_test')
74     @tmpdir = @tmpfile.path
75     @tmpfile.close!
76     Dir.mkdir(@tmpdir)
77     Dir.chdir(@tmpdir)
78     @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
79     @port = unused_port(@addr)
80     @sockets = []
81     @start_pid = $$
82   end
84   def teardown
85     return if @start_pid != $$
86     Dir.chdir(@pwd)
87     FileUtils.rmtree(@tmpdir)
88     @sockets.each { |path| File.unlink(path) rescue nil }
89     loop do
90       Process.kill('-QUIT', 0)
91       begin
92         Process.waitpid(-1, Process::WNOHANG) or break
93       rescue Errno::ECHILD
94         break
95       end
96     end
97   end
99   def test_sd_listen_fds_emulation
100     File.open("config.ru", "wb") { |fp| fp.write(HI) }
101     sock = TCPServer.new(@addr, @port)
103     [ %W(-l #@addr:#@port), nil ].each do |l|
104       sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
106       pid = xfork do
107         redirect_test_io do
108           # pretend to be systemd
109           ENV['LISTEN_PID'] = "#$$"
110           ENV['LISTEN_FDS'] = '1'
112           # 3 = SD_LISTEN_FDS_START
113           args = [ $unicorn_bin ]
114           args.concat(l) if l
115           args << { 3 => sock }
116           exec(*args)
117         end
118       end
119       res = hit(["http://#@addr:#@port/"])
120       assert_equal [ "HI\n" ], res
121       assert_shutdown(pid)
122       assert_equal 1, sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).int,
123                   'unicorn should always set SO_KEEPALIVE on inherited sockets'
124     end
125   ensure
126     sock.close if sock
127     # disabled test on old Rubies: https://bugs.ruby-lang.org/issues/11336
128     # [ruby-core:69895] [Bug #11336] fixed by r51576
129   end if RUBY_VERSION.to_f >= 2.3
131   def test_working_directory_rel_path_config_file
132     other = Tempfile.new('unicorn.wd')
133     File.unlink(other.path)
134     Dir.mkdir(other.path)
135     File.open("config.ru", "wb") do |fp|
136       fp.syswrite WORKING_DIRECTORY_CHECK_RU
137     end
138     FileUtils.cp("config.ru", other.path + "/config.ru")
139     Dir.chdir(@tmpdir)
141     tmp = File.open('unicorn.config', 'wb')
142     tmp.syswrite <<EOF
143 working_directory '#@tmpdir'
144 listen '#@addr:#@port'
146     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
147     wait_workers_ready("test_stderr.#{pid}.log", 1)
148     results = hit(["http://#@addr:#@port/"])
149     assert_equal @tmpdir, results.first
150     File.truncate("test_stderr.#{pid}.log", 0)
152     tmp.sysseek(0)
153     tmp.truncate(0)
154     tmp.syswrite <<EOF
155 working_directory '#{other.path}'
156 listen '#@addr:#@port'
159     Process.kill(:HUP, pid)
160     lines = []
161     re = /config_file=(.+) would not be accessible in working_directory=(.+)/
162     until lines.grep(re)
163       sleep 0.1
164       lines = File.readlines("test_stderr.#{pid}.log")
165     end
167     File.truncate("test_stderr.#{pid}.log", 0)
168     FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
169     Process.kill(:HUP, pid)
170     wait_workers_ready("test_stderr.#{pid}.log", 1)
171     results = hit(["http://#@addr:#@port/"])
172     assert_equal other.path, results.first
174     Process.kill(:QUIT, pid)
175     ensure
176       FileUtils.rmtree(other.path)
177   end
179   def test_working_directory
180     other = Tempfile.new('unicorn.wd')
181     File.unlink(other.path)
182     Dir.mkdir(other.path)
183     File.open("config.ru", "wb") do |fp|
184       fp.syswrite WORKING_DIRECTORY_CHECK_RU
185     end
186     FileUtils.cp("config.ru", other.path + "/config.ru")
187     tmp = Tempfile.new('unicorn.config')
188     tmp.syswrite <<EOF
189 working_directory '#@tmpdir'
190 listen '#@addr:#@port'
192     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
193     wait_workers_ready("test_stderr.#{pid}.log", 1)
194     results = hit(["http://#@addr:#@port/"])
195     assert_equal @tmpdir, results.first
196     File.truncate("test_stderr.#{pid}.log", 0)
198     tmp.sysseek(0)
199     tmp.truncate(0)
200     tmp.syswrite <<EOF
201 working_directory '#{other.path}'
202 listen '#@addr:#@port'
205     Process.kill(:HUP, pid)
206     wait_workers_ready("test_stderr.#{pid}.log", 1)
207     results = hit(["http://#@addr:#@port/"])
208     assert_equal other.path, results.first
210     Process.kill(:QUIT, pid)
211     ensure
212       FileUtils.rmtree(other.path)
213   end
215   def test_working_directory_controls_relative_paths
216     other = Tempfile.new('unicorn.wd')
217     File.unlink(other.path)
218     Dir.mkdir(other.path)
219     File.open("config.ru", "wb") do |fp|
220       fp.syswrite WORKING_DIRECTORY_CHECK_RU
221     end
222     FileUtils.cp("config.ru", other.path + "/config.ru")
223     system('mkfifo', "#{other.path}/fifo")
224     tmp = Tempfile.new('unicorn.config')
225     tmp.syswrite <<EOF
226 pid "pid_file_here"
227 stderr_path "stderr_log_here"
228 stdout_path "stdout_log_here"
229 working_directory '#{other.path}'
230 listen '#@addr:#@port'
231 after_fork do |server, worker|
232   File.open("fifo", "wb").close
235     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
236     File.open("#{other.path}/fifo", "rb").close
238     assert ! File.exist?("stderr_log_here")
239     assert ! File.exist?("stdout_log_here")
240     assert ! File.exist?("pid_file_here")
242     assert ! File.exist?("#@tmpdir/stderr_log_here")
243     assert ! File.exist?("#@tmpdir/stdout_log_here")
244     assert ! File.exist?("#@tmpdir/pid_file_here")
246     assert File.exist?("#{other.path}/pid_file_here")
247     assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
248     assert File.exist?("#{other.path}/stderr_log_here")
249     assert File.exist?("#{other.path}/stdout_log_here")
250     wait_master_ready("#{other.path}/stderr_log_here")
252     Process.kill(:QUIT, pid)
253     ensure
254       FileUtils.rmtree(other.path)
255   end
258   def test_exit_signals
259     %w(INT TERM QUIT).each do |sig|
260       File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
261       pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
262       wait_master_ready("test_stderr.#{pid}.log")
263       wait_workers_ready("test_stderr.#{pid}.log", 1)
265       Process.kill(sig, pid)
266       pid, status = Process.waitpid2(pid)
268       reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
269       assert_equal 1, reaped.size
270       assert status.exited?
271     end
272   end
274   def test_basic
275     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
276     pid = fork do
277       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
278     end
279     results = retry_hit(["http://#{@addr}:#{@port}/"])
280     assert_equal String, results[0].class
281     assert_shutdown(pid)
282   end
284   def test_rack_env_unset
285     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
286     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
287     results = retry_hit(["http://#{@addr}:#{@port}/"])
288     assert_equal "development", results.first
289     assert_shutdown(pid)
290   end
292   def test_rack_env_cli_set
293     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
294     pid = fork {
295       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
296     }
297     results = retry_hit(["http://#{@addr}:#{@port}/"])
298     assert_equal "asdf", results.first
299     assert_shutdown(pid)
300   end
302   def test_rack_env_ENV_set
303     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
304     pid = fork {
305       ENV["RACK_ENV"] = "foobar"
306       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
307     }
308     results = retry_hit(["http://#{@addr}:#{@port}/"])
309     assert_equal "foobar", results.first
310     assert_shutdown(pid)
311   end
313   def test_rack_env_cli_override_ENV
314     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
315     pid = fork {
316       ENV["RACK_ENV"] = "foobar"
317       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
318     }
319     results = retry_hit(["http://#{@addr}:#{@port}/"])
320     assert_equal "asdf", results.first
321     assert_shutdown(pid)
322   end
324   def test_ttin_ttou
325     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
326     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
327     log = "test_stderr.#{pid}.log"
328     wait_master_ready(log)
329     [ 2, 3].each { |i|
330       Process.kill(:TTIN, pid)
331       wait_workers_ready(log, i)
332     }
333     File.truncate(log, 0)
334     reaped = nil
335     [ 2, 1, 0].each { |i|
336       Process.kill(:TTOU, pid)
337       DEFAULT_TRIES.times {
338         sleep DEFAULT_RES
339         reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
340         break if reaped.size == 1
341       }
342       assert_equal 1, reaped.size
343     }
344   end
346   def test_help
347     redirect_test_io do
348       assert(system($unicorn_bin, "-h"), "help text returns true")
349     end
350     assert_equal 0, File.stat("test_stderr.#$$.log").size
351     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
352     lines = File.readlines("test_stdout.#$$.log")
354     # Be considerate of the on-call technician working from their
355     # mobile phone or netbook on a slow connection :)
356     assert lines.size <= 24, "help height fits in an ANSI terminal window"
357     lines.each do |line|
358       line.chomp!
359       assert line.size <= 80, "help width fits in an ANSI terminal window"
360     end
361   end
363   def test_broken_reexec_config
364     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
365     pid_file = "#{@tmpdir}/test.pid"
366     old_file = "#{pid_file}.oldbin"
367     ucfg = Tempfile.new('unicorn_test_config')
368     ucfg.syswrite("listen %(#@addr:#@port)\n")
369     ucfg.syswrite("pid %(#{pid_file})\n")
370     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
371     pid = xfork do
372       redirect_test_io do
373         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
374       end
375     end
376     results = retry_hit(["http://#{@addr}:#{@port}/"])
377     assert_equal String, results[0].class
379     wait_for_file(pid_file)
380     Process.waitpid(pid)
381     Process.kill(:USR2, File.read(pid_file).to_i)
382     wait_for_file(old_file)
383     wait_for_file(pid_file)
384     old_pid = File.read(old_file).to_i
385     Process.kill(:QUIT, old_pid)
386     wait_for_death(old_pid)
388     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
389     current_pid = File.read(pid_file).to_i
390     Process.kill(:USR2, current_pid)
392     # wait for pid_file to restore itself
393     tries = DEFAULT_TRIES
394     begin
395       while current_pid != File.read(pid_file).to_i
396         sleep(DEFAULT_RES) and (tries -= 1) > 0
397       end
398     rescue Errno::ENOENT
399       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
400     end
401     assert_equal current_pid, File.read(pid_file).to_i
403     tries = DEFAULT_TRIES
404     while File.exist?(old_file)
405       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
406     end
407     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
408     port2 = unused_port(@addr)
410     # fix the bug
411     ucfg.sysseek(0)
412     ucfg.truncate(0)
413     ucfg.syswrite("listen %(#@addr:#@port)\n")
414     ucfg.syswrite("listen %(#@addr:#{port2})\n")
415     ucfg.syswrite("pid %(#{pid_file})\n")
416     Process.kill(:USR2, current_pid)
418     wait_for_file(old_file)
419     wait_for_file(pid_file)
420     new_pid = File.read(pid_file).to_i
421     assert_not_equal current_pid, new_pid
422     assert_equal current_pid, File.read(old_file).to_i
423     results = retry_hit(["http://#{@addr}:#{@port}/",
424                          "http://#{@addr}:#{port2}/"])
425     assert_equal String, results[0].class
426     assert_equal String, results[1].class
428     Process.kill(:QUIT, current_pid)
429     Process.kill(:QUIT, new_pid)
430   end
432   def test_broken_reexec_ru
433     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
434     pid_file = "#{@tmpdir}/test.pid"
435     old_file = "#{pid_file}.oldbin"
436     ucfg = Tempfile.new('unicorn_test_config')
437     ucfg.syswrite("pid %(#{pid_file})\n")
438     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
439     pid = xfork do
440       redirect_test_io do
441         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
442       end
443     end
444     results = retry_hit(["http://#{@addr}:#{@port}/"])
445     assert_equal String, results[0].class
447     wait_for_file(pid_file)
448     Process.waitpid(pid)
449     Process.kill(:USR2, File.read(pid_file).to_i)
450     wait_for_file(old_file)
451     wait_for_file(pid_file)
452     old_pid = File.read(old_file).to_i
453     Process.kill(:QUIT, old_pid)
454     wait_for_death(old_pid)
456     File.unlink("config.ru") # break reloading
457     current_pid = File.read(pid_file).to_i
458     Process.kill(:USR2, current_pid)
460     # wait for pid_file to restore itself
461     tries = DEFAULT_TRIES
462     begin
463       while current_pid != File.read(pid_file).to_i
464         sleep(DEFAULT_RES) and (tries -= 1) > 0
465       end
466     rescue Errno::ENOENT
467       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
468     end
470     tries = DEFAULT_TRIES
471     while File.exist?(old_file)
472       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
473     end
474     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
475     assert_equal current_pid, File.read(pid_file).to_i
477     # fix the bug
478     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
479     Process.kill(:USR2, current_pid)
480     wait_for_file(old_file)
481     wait_for_file(pid_file)
482     new_pid = File.read(pid_file).to_i
483     assert_not_equal current_pid, new_pid
484     assert_equal current_pid, File.read(old_file).to_i
485     results = retry_hit(["http://#{@addr}:#{@port}/"])
486     assert_equal String, results[0].class
488     Process.kill(:QUIT, current_pid)
489     Process.kill(:QUIT, new_pid)
490   end
492   def test_unicorn_config_listener_swap
493     port_cli = unused_port
494     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
495     ucfg = Tempfile.new('unicorn_test_config')
496     ucfg.syswrite("listen '#@addr:#@port'\n")
497     pid = xfork do
498       redirect_test_io do
499         exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
500       end
501     end
502     results = retry_hit(["http://#@addr:#{port_cli}/"])
503     assert_equal String, results[0].class
504     results = retry_hit(["http://#@addr:#@port/"])
505     assert_equal String, results[0].class
507     port2 = unused_port(@addr)
508     ucfg.sysseek(0)
509     ucfg.truncate(0)
510     ucfg.syswrite("listen '#@addr:#{port2}'\n")
511     Process.kill(:HUP, pid)
513     results = retry_hit(["http://#@addr:#{port2}/"])
514     assert_equal String, results[0].class
515     results = retry_hit(["http://#@addr:#{port_cli}/"])
516     assert_equal String, results[0].class
517     reuse = TCPServer.new(@addr, @port)
518     reuse.close
519     assert_shutdown(pid)
520   end
522   def test_unicorn_config_listen_with_options
523     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
524     ucfg = Tempfile.new('unicorn_test_config')
525     ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
526     ucfg.syswrite("                            :rcvbuf => 4096,\n")
527     ucfg.syswrite("                            :sndbuf => 4096\n")
528     pid = xfork do
529       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
530     end
531     results = retry_hit(["http://#{@addr}:#{@port}/"])
532     assert_equal String, results[0].class
533     assert_shutdown(pid)
534   end
536   def test_unicorn_config_per_worker_listen
537     port2 = unused_port
538     pid_spit = 'use Rack::ContentLength;' \
539       'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
540     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
541     tmp = Tempfile.new('test.socket')
542     File.unlink(tmp.path)
543     ucfg = Tempfile.new('unicorn_test_config')
544     ucfg.syswrite("listen '#@addr:#@port'\n")
545     ucfg.syswrite("after_fork { |s,w|\n")
546     ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
547     ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
548     ucfg.syswrite("\n}\n")
549     pid = xfork do
550       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
551     end
552     results = retry_hit(["http://#{@addr}:#{@port}/"])
553     assert_equal String, results[0].class
554     worker_pid = results[0].to_i
555     assert_not_equal pid, worker_pid
556     s = UNIXSocket.new(tmp.path)
557     s.syswrite("GET / HTTP/1.0\r\n\r\n")
558     results = ''
559     loop { results << s.sysread(4096) } rescue nil
560     s.close
561     assert_equal worker_pid, results.split(/\r\n/).last.to_i
562     results = hit(["http://#@addr:#{port2}/"])
563     assert_equal String, results[0].class
564     assert_equal worker_pid, results[0].to_i
565     assert_shutdown(pid)
566   end
568   def test_unicorn_config_listen_augments_cli
569     port2 = unused_port(@addr)
570     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
571     ucfg = Tempfile.new('unicorn_test_config')
572     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
573     pid = xfork do
574       redirect_test_io do
575         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
576       end
577     end
578     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
579     results = retry_hit(uris)
580     assert_equal results.size, uris.size
581     assert_equal String, results[0].class
582     assert_equal String, results[1].class
583     assert_shutdown(pid)
584   end
586   def test_weird_config_settings
587     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
588     ucfg = Tempfile.new('unicorn_test_config')
589     ucfg.syswrite(HEAVY_CFG)
590     pid = xfork do
591       redirect_test_io do
592         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
593       end
594     end
596     results = retry_hit(["http://#{@addr}:#{@port}/"])
597     assert_equal String, results[0].class
598     wait_master_ready(COMMON_TMP.path)
599     wait_workers_ready(COMMON_TMP.path, 4)
600     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
601     assert_equal 4, bf.size
602     rotate = Tempfile.new('unicorn_rotate')
604     File.rename(COMMON_TMP.path, rotate.path)
605     Process.kill(:USR1, pid)
607     wait_for_file(COMMON_TMP.path)
608     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
609     # USR1 should've been passed to all workers
610     tries = DEFAULT_TRIES
611     log = File.readlines(rotate.path)
612     while (tries -= 1) > 0 &&
613           log.grep(/reopening logs\.\.\./).size < 5
614       sleep DEFAULT_RES
615       log = File.readlines(rotate.path)
616     end
617     assert_equal 5, log.grep(/reopening logs\.\.\./).size
618     assert_equal 0, log.grep(/done reopening logs/).size
620     tries = DEFAULT_TRIES
621     log = File.readlines(COMMON_TMP.path)
622     while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
623       sleep DEFAULT_RES
624       log = File.readlines(COMMON_TMP.path)
625     end
626     assert_equal 5, log.grep(/done reopening logs/).size
627     assert_equal 0, log.grep(/reopening logs\.\.\./).size
629     Process.kill(:QUIT, pid)
630     pid, status = Process.waitpid2(pid)
632     assert status.success?, "exited successfully"
633   end
635   def test_read_embedded_cli_switches
636     File.open("config.ru", "wb") do |fp|
637       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
638       fp.syswrite(HI)
639     end
640     pid = fork { redirect_test_io { exec($unicorn_bin) } }
641     results = retry_hit(["http://#{@addr}:#{@port}/"])
642     assert_equal String, results[0].class
643     assert_shutdown(pid)
644   end
646   def test_config_ru_alt_path
647     config_path = "#{@tmpdir}/foo.ru"
648     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
649     pid = fork do
650       redirect_test_io do
651         Dir.chdir("/")
652         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
653       end
654     end
655     results = retry_hit(["http://#{@addr}:#{@port}/"])
656     assert_equal String, results[0].class
657     assert_shutdown(pid)
658   end
660   def test_load_module
661     libdir = "#{@tmpdir}/lib"
662     FileUtils.mkpath([ libdir ])
663     config_path = "#{libdir}/hello.rb"
664     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
665     pid = fork do
666       redirect_test_io do
667         Dir.chdir("/")
668         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
669       end
670     end
671     results = retry_hit(["http://#{@addr}:#{@port}/"])
672     assert_equal String, results[0].class
673     assert_shutdown(pid)
674   end
676   def test_reexec
677     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
678     pid_file = "#{@tmpdir}/test.pid"
679     pid = fork do
680       redirect_test_io do
681         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
682       end
683     end
684     reexec_basic_test(pid, pid_file)
685   end
687   def test_reexec_alt_config
688     config_file = "#{@tmpdir}/foo.ru"
689     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
690     pid_file = "#{@tmpdir}/test.pid"
691     pid = fork do
692       redirect_test_io do
693         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
694       end
695     end
696     reexec_basic_test(pid, pid_file)
697   end
699   def test_socket_unlinked_restore
700     results = nil
701     sock = Tempfile.new('unicorn_test_sock')
702     sock_path = sock.path
703     @sockets << sock_path
704     sock.close!
705     ucfg = Tempfile.new('unicorn_test_config')
706     ucfg.syswrite("listen \"#{sock_path}\"\n")
708     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
709     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
710     wait_for_file(sock_path)
711     assert File.socket?(sock_path)
713     sock = UNIXSocket.new(sock_path)
714     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
715     results = sock.sysread(4096)
717     assert_equal String, results.class
718     File.unlink(sock_path)
719     Process.kill(:HUP, pid)
720     wait_for_file(sock_path)
721     assert File.socket?(sock_path)
723     sock = UNIXSocket.new(sock_path)
724     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
725     results = sock.sysread(4096)
727     assert_equal String, results.class
728   end
730   def test_unicorn_config_file
731     pid_file = "#{@tmpdir}/test.pid"
732     sock = Tempfile.new('unicorn_test_sock')
733     sock_path = sock.path
734     sock.close!
735     @sockets << sock_path
737     log = Tempfile.new('unicorn_test_log')
738     ucfg = Tempfile.new('unicorn_test_config')
739     ucfg.syswrite("listen \"#{sock_path}\"\n")
740     ucfg.syswrite("pid \"#{pid_file}\"\n")
741     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
742     ucfg.close
744     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
745     pid = xfork do
746       redirect_test_io do
747         exec($unicorn_bin, "-l#{@addr}:#{@port}",
748              "-P#{pid_file}", "-c#{ucfg.path}")
749       end
750     end
751     results = retry_hit(["http://#{@addr}:#{@port}/"])
752     assert_equal String, results[0].class
753     wait_master_ready(log.path)
754     assert File.exist?(pid_file), "pid_file created"
755     assert_equal pid, File.read(pid_file).to_i
756     assert File.socket?(sock_path), "socket created"
758     sock = UNIXSocket.new(sock_path)
759     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
760     results = sock.sysread(4096)
762     assert_equal String, results.class
764     # try reloading the config
765     sock = Tempfile.new('new_test_sock')
766     new_sock_path = sock.path
767     @sockets << new_sock_path
768     sock.close!
769     new_log = Tempfile.new('unicorn_test_log')
770     new_log.sync = true
771     assert_equal 0, new_log.size
773     ucfg = File.open(ucfg.path, "wb")
774     ucfg.syswrite("listen \"#{sock_path}\"\n")
775     ucfg.syswrite("listen \"#{new_sock_path}\"\n")
776     ucfg.syswrite("pid \"#{pid_file}\"\n")
777     ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
778     ucfg.close
779     Process.kill(:HUP, pid)
781     wait_for_file(new_sock_path)
782     assert File.socket?(new_sock_path), "socket exists"
783     @sockets.each do |path|
784       sock = UNIXSocket.new(path)
785       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
786       results = sock.sysread(4096)
787       assert_equal String, results.class
788     end
790     assert_not_equal 0, new_log.size
791     reexec_usr2_quit_test(pid, pid_file)
792   end
794   def test_daemonize_reexec
795     pid_file = "#{@tmpdir}/test.pid"
796     log = Tempfile.new('unicorn_test_log')
797     ucfg = Tempfile.new('unicorn_test_config')
798     ucfg.syswrite("pid \"#{pid_file}\"\n")
799     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
800     ucfg.close
802     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
803     pid = xfork do
804       redirect_test_io do
805         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
806       end
807     end
808     results = retry_hit(["http://#{@addr}:#{@port}/"])
809     assert_equal String, results[0].class
810     wait_for_file(pid_file)
811     new_pid = File.read(pid_file).to_i
812     assert_not_equal pid, new_pid
813     pid, status = Process.waitpid2(pid)
814     assert status.success?, "original process exited successfully"
815     Process.kill(0, new_pid)
816     reexec_usr2_quit_test(new_pid, pid_file)
817   end
819   def test_daemonize_redirect_fail
820     pid_file = "#{@tmpdir}/test.pid"
821     ucfg = Tempfile.new('unicorn_test_config')
822     ucfg.syswrite("pid #{pid_file}\"\n")
823     err = Tempfile.new('stderr')
824     out = Tempfile.new('stdout ')
826     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
827     pid = xfork do
828       $stderr.reopen(err.path, "a")
829       $stdout.reopen(out.path, "a")
830       exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
831     end
832     pid, status = Process.waitpid2(pid)
833     assert ! status.success?, "original process exited successfully"
834     sleep 1 # can't waitpid on a daemonized process :<
835     assert err.stat.size > 0
836   end
838   def test_reexec_fd_leak
839     unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
840       warn "FD leak test only works on Linux at the moment"
841       return
842     end
843     pid_file = "#{@tmpdir}/test.pid"
844     log = Tempfile.new('unicorn_test_log')
845     log.sync = true
846     ucfg = Tempfile.new('unicorn_test_config')
847     ucfg.syswrite("pid \"#{pid_file}\"\n")
848     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
849     ucfg.syswrite("stderr_path '#{log.path}'\n")
850     ucfg.syswrite("stdout_path '#{log.path}'\n")
851     ucfg.close
853     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
854     pid = xfork do
855       redirect_test_io do
856         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
857       end
858     end
860     wait_master_ready(log.path)
861     wait_workers_ready(log.path, 1)
862     File.truncate(log.path, 0)
863     wait_for_file(pid_file)
864     orig_pid = pid = File.read(pid_file).to_i
865     orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
866     assert $?.success?
867     expect_size = orig_fds.size
869     Process.kill(:USR2, pid)
870     wait_for_file("#{pid_file}.oldbin")
871     Process.kill(:QUIT, pid)
873     wait_for_death(pid)
875     wait_master_ready(log.path)
876     wait_workers_ready(log.path, 1)
877     File.truncate(log.path, 0)
878     wait_for_file(pid_file)
879     pid = File.read(pid_file).to_i
880     assert_not_equal orig_pid, pid
881     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
882     assert $?.success?
884     # we could've inherited descriptors the first time around
885     assert expect_size >= curr_fds.size, curr_fds.inspect
886     expect_size = curr_fds.size
888     Process.kill(:USR2, pid)
889     wait_for_file("#{pid_file}.oldbin")
890     Process.kill(:QUIT, pid)
892     wait_for_death(pid)
894     wait_master_ready(log.path)
895     wait_workers_ready(log.path, 1)
896     File.truncate(log.path, 0)
897     wait_for_file(pid_file)
898     pid = File.read(pid_file).to_i
899     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
900     assert $?.success?
901     assert_equal expect_size, curr_fds.size, curr_fds.inspect
903     Process.kill(:QUIT, pid)
904     wait_for_death(pid)
905   end
907   def hup_test_common(preload, check_client=false)
908     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
909     pid_file = Tempfile.new('pid')
910     ucfg = Tempfile.new('unicorn_test_config')
911     ucfg.syswrite("listen '#@addr:#@port'\n")
912     ucfg.syswrite("pid '#{pid_file.path}'\n")
913     ucfg.syswrite("preload_app true\n") if preload
914     ucfg.syswrite("check_client_connection true\n") if check_client
915     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
916     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
917     pid = xfork {
918       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
919     }
920     _, status = Process.waitpid2(pid)
921     assert status.success?
922     wait_master_ready("test_stderr.#$$.log")
923     wait_workers_ready("test_stderr.#$$.log", 1)
924     uri = URI.parse("http://#@addr:#@port/")
925     pids = Tempfile.new('worker_pids')
926     r, w = IO.pipe
927     hitter = fork {
928       r.close
929       bodies = Hash.new(0)
930       at_exit { pids.syswrite(bodies.inspect) }
931       trap(:TERM) { exit(0) }
932       nr = 0
933       loop {
934         rv = Net::HTTP.get(uri)
935         pid = rv.to_i
936         exit!(1) if pid <= 0
937         bodies[pid] += 1
938         nr += 1
939         if nr == 1
940           w.syswrite('1')
941         elsif bodies.size > 1
942           w.syswrite('2')
943           sleep
944         end
945       }
946     }
947     w.close
948     assert_equal '1', r.read(1)
949     daemon_pid = File.read(pid_file.path).to_i
950     assert daemon_pid > 0
951     Process.kill(:HUP, daemon_pid)
952     assert_equal '2', r.read(1)
953     Process.kill(:TERM, hitter)
954     _, hitter_status = Process.waitpid2(hitter)
955     assert(hitter_status.success?,
956            "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
957            "#{File.read("test_stderr.#$$.log")}")
958     pids.sysseek(0)
959     pids = eval(pids.read)
960     assert_kind_of(Hash, pids)
961     assert_equal 2, pids.size
962     pids.keys.each { |x|
963       assert_kind_of(Integer, x)
964       assert x > 0
965       assert pids[x] > 0
966     }
967     Process.kill(:QUIT, daemon_pid)
968     wait_for_death(daemon_pid)
969   end
971   def test_preload_app_hup
972     hup_test_common(true)
973   end
975   def test_hup
976     hup_test_common(false)
977   end
979   def test_check_client_hup
980     hup_test_common(false, true)
981   end
983   def test_default_listen_hup_holds_listener
984     default_listen_lock do
985       res, pid_path = default_listen_setup
986       daemon_pid = File.read(pid_path).to_i
987       Process.kill(:HUP, daemon_pid)
988       wait_workers_ready("test_stderr.#$$.log", 1)
989       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
990       assert_match %r{\d+}, res2.first
991       assert res2.first != res.first
992       Process.kill(:QUIT, daemon_pid)
993       wait_for_death(daemon_pid)
994     end
995   end
997   def test_default_listen_upgrade_holds_listener
998     default_listen_lock do
999       res, pid_path = default_listen_setup
1000       daemon_pid = File.read(pid_path).to_i
1002       Process.kill(:USR2, daemon_pid)
1003       wait_for_file("#{pid_path}.oldbin")
1004       wait_for_file(pid_path)
1005       Process.kill(:QUIT, daemon_pid)
1006       wait_for_death(daemon_pid)
1008       daemon_pid = File.read(pid_path).to_i
1009       wait_workers_ready("test_stderr.#$$.log", 1)
1010       File.truncate("test_stderr.#$$.log", 0)
1012       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1013       assert_match %r{\d+}, res2.first
1014       assert res2.first != res.first
1016       Process.kill(:HUP, daemon_pid)
1017       wait_workers_ready("test_stderr.#$$.log", 1)
1018       File.truncate("test_stderr.#$$.log", 0)
1019       res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1020       assert res2.first != res3.first
1022       Process.kill(:QUIT, daemon_pid)
1023       wait_for_death(daemon_pid)
1024     end
1025   end
1027   def default_listen_setup
1028     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
1029     pid_path = (tmp = Tempfile.new('pid')).path
1030     tmp.close!
1031     ucfg = Tempfile.new('unicorn_test_config')
1032     ucfg.syswrite("pid '#{pid_path}'\n")
1033     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1034     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1035     pid = xfork {
1036       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1037     }
1038     _, status = Process.waitpid2(pid)
1039     assert status.success?
1040     wait_master_ready("test_stderr.#$$.log")
1041     wait_workers_ready("test_stderr.#$$.log", 1)
1042     File.truncate("test_stderr.#$$.log", 0)
1043     res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1044     assert_match %r{\d+}, res.first
1045     [ res, pid_path ]
1046   end
1048   # we need to flock() something to prevent these tests from running
1049   def default_listen_lock(&block)
1050     fp = File.open(FLOCK_PATH, "rb")
1051     begin
1052       fp.flock(File::LOCK_EX)
1053       begin
1054         TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1055                       Unicorn::Const::DEFAULT_PORT).close
1056       rescue Errno::EADDRINUSE, Errno::EACCES
1057         warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1058         return false
1059       end
1061       # unused_port should never take this, but we may run an environment
1062       # where tests are being run against older unicorns...
1063       lock_path = "#{Dir::tmpdir}/unicorn_test." \
1064                   "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1065       begin
1066         File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1067         yield
1068       rescue Errno::EEXIST
1069         lock_path = nil
1070         return false
1071       ensure
1072         File.unlink(lock_path) if lock_path
1073       end
1074     ensure
1075       fp.flock(File::LOCK_UN)
1076     end
1077   end
1079 end if do_test