1 # -*- encoding: binary -*-
3 # Copyright (c) 2009 Eric Wong
4 FLOCK_PATH = File.expand_path(__FILE__)
5 require './test/test_helper'
8 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
10 do_test = system($unicorn_bin, '-v')
14 warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
18 unless try_require('rack')
19 warn "Unable to load Rack, skipping this test"
23 class ExecTest < Test::Unit::TestCase
27 use Rack::ContentLength
28 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
31 SHOW_RACK_ENV = <<-EOS
32 use Rack::ContentLength
34 [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
41 [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
46 COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
51 logger Logger.new('#{COMMON_TMP.path}')
52 before_fork do |server, worker|
53 server.logger.info "before_fork: worker=\#{worker.nr}"
57 WORKING_DIRECTORY_CHECK_RU = <<-EOS
58 use Rack::ContentLength
62 b = ::File.stat(Dir.pwd)
63 if (a.ino == b.ino && a.dev == b.dev)
64 [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ]
66 [ 404, { 'Content-Type' => 'text/plain' }, [] ]
73 @tmpfile = Tempfile.new('unicorn_exec_test')
74 @tmpdir = @tmpfile.path
78 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
79 @port = unused_port(@addr)
85 return if @start_pid != $$
87 FileUtils.rmtree(@tmpdir)
88 @sockets.each { |path| File.unlink(path) rescue nil }
90 Process.kill('-QUIT', 0)
92 Process.waitpid(-1, Process::WNOHANG) or break
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)
108 # pretend to be systemd
109 ENV['LISTEN_PID'] = "#$$"
110 ENV['LISTEN_FDS'] = '1'
112 # 3 = SD_LISTEN_FDS_START
113 args = [ $unicorn_bin ]
115 args << { 3 => sock }
119 res = hit(["http://#@addr:#@port/"])
120 assert_equal [ "HI\n" ], res
122 assert_equal 1, sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).int,
123 'unicorn should always set SO_KEEPALIVE on inherited sockets'
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
138 FileUtils.cp("config.ru", other.path + "/config.ru")
141 tmp = File.open('unicorn.config', 'wb')
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)
155 working_directory '#{other.path}'
156 listen '#@addr:#@port'
159 Process.kill(:HUP, pid)
161 re = /config_file=(.+) would not be accessible in working_directory=(.+)/
164 lines = File.readlines("test_stderr.#{pid}.log")
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)
176 FileUtils.rmtree(other.path)
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
186 FileUtils.cp("config.ru", other.path + "/config.ru")
187 tmp = Tempfile.new('unicorn.config')
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)
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)
212 FileUtils.rmtree(other.path)
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
222 FileUtils.cp("config.ru", other.path + "/config.ru")
223 system('mkfifo', "#{other.path}/fifo")
224 tmp = Tempfile.new('unicorn.config')
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)
254 FileUtils.rmtree(other.path)
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?
275 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
277 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
279 results = retry_hit(["http://#{@addr}:#{@port}/"])
280 assert_equal String, results[0].class
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
292 def test_rack_env_cli_set
293 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
295 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
297 results = retry_hit(["http://#{@addr}:#{@port}/"])
298 assert_equal "asdf", results.first
302 def test_rack_env_ENV_set
303 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
305 ENV["RACK_ENV"] = "foobar"
306 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
308 results = retry_hit(["http://#{@addr}:#{@port}/"])
309 assert_equal "foobar", results.first
313 def test_rack_env_cli_override_ENV
314 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
316 ENV["RACK_ENV"] = "foobar"
317 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
319 results = retry_hit(["http://#{@addr}:#{@port}/"])
320 assert_equal "asdf", results.first
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)
330 Process.kill(:TTIN, pid)
331 wait_workers_ready(log, i)
333 File.truncate(log, 0)
335 [ 2, 1, 0].each { |i|
336 Process.kill(:TTOU, pid)
337 DEFAULT_TRIES.times {
339 reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
340 break if reaped.size == 1
342 assert_equal 1, reaped.size
348 assert(system($unicorn_bin, "-h"), "help text returns true")
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"
359 assert line.size <= 80, "help width fits in an ANSI terminal window"
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")
373 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
376 results = retry_hit(["http://#{@addr}:#{@port}/"])
377 assert_equal String, results[0].class
379 wait_for_file(pid_file)
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
395 while current_pid != File.read(pid_file).to_i
396 sleep(DEFAULT_RES) and (tries -= 1) > 0
399 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
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
407 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
408 port2 = unused_port(@addr)
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)
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")
441 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
444 results = retry_hit(["http://#{@addr}:#{@port}/"])
445 assert_equal String, results[0].class
447 wait_for_file(pid_file)
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
463 while current_pid != File.read(pid_file).to_i
464 sleep(DEFAULT_RES) and (tries -= 1) > 0
467 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
470 tries = DEFAULT_TRIES
471 while File.exist?(old_file)
472 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
474 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
475 assert_equal current_pid, File.read(pid_file).to_i
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)
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")
499 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
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)
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)
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")
529 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
531 results = retry_hit(["http://#{@addr}:#{@port}/"])
532 assert_equal String, results[0].class
536 def test_unicorn_config_per_worker_listen
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")
550 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
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")
559 loop { results << s.sysread(4096) } rescue nil
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
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")
575 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
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
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)
592 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
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
615 log = File.readlines(rotate.path)
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
624 log = File.readlines(COMMON_TMP.path)
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"
635 def test_read_embedded_cli_switches
636 File.open("config.ru", "wb") do |fp|
637 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
640 pid = fork { redirect_test_io { exec($unicorn_bin) } }
641 results = retry_hit(["http://#{@addr}:#{@port}/"])
642 assert_equal String, results[0].class
646 def test_config_ru_alt_path
647 config_path = "#{@tmpdir}/foo.ru"
648 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
652 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
655 results = retry_hit(["http://#{@addr}:#{@port}/"])
656 assert_equal String, results[0].class
661 libdir = "#{@tmpdir}/lib"
662 FileUtils.mkpath([ libdir ])
663 config_path = "#{libdir}/hello.rb"
664 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
668 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
671 results = retry_hit(["http://#{@addr}:#{@port}/"])
672 assert_equal String, results[0].class
677 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
678 pid_file = "#{@tmpdir}/test.pid"
681 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
684 reexec_basic_test(pid, pid_file)
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"
693 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
696 reexec_basic_test(pid, pid_file)
699 def test_socket_unlinked_restore
701 sock = Tempfile.new('unicorn_test_sock')
702 sock_path = sock.path
703 @sockets << sock_path
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
730 def test_unicorn_config_file
731 pid_file = "#{@tmpdir}/test.pid"
732 sock = Tempfile.new('unicorn_test_sock')
733 sock_path = sock.path
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")
744 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
747 exec($unicorn_bin, "-l#{@addr}:#{@port}",
748 "-P#{pid_file}", "-c#{ucfg.path}")
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
769 new_log = Tempfile.new('unicorn_test_log')
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")
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
790 assert_not_equal 0, new_log.size
791 reexec_usr2_quit_test(pid, pid_file)
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")
802 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
805 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
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)
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) }
828 $stderr.reopen(err.path, "a")
829 $stdout.reopen(out.path, "a")
830 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
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
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"
843 pid_file = "#{@tmpdir}/test.pid"
844 log = Tempfile.new('unicorn_test_log')
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")
853 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
856 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
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/)
867 expect_size = orig_fds.size
869 Process.kill(:USR2, pid)
870 wait_for_file("#{pid_file}.oldbin")
871 Process.kill(:QUIT, 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/)
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)
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/)
901 assert_equal expect_size, curr_fds.size, curr_fds.inspect
903 Process.kill(:QUIT, pid)
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")
918 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
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')
930 at_exit { pids.syswrite(bodies.inspect) }
931 trap(:TERM) { exit(0) }
934 rv = Net::HTTP.get(uri)
941 elsif bodies.size > 1
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")}")
959 pids = eval(pids.read)
960 assert_kind_of(Hash, pids)
961 assert_equal 2, pids.size
963 assert_kind_of(Integer, x)
967 Process.kill(:QUIT, daemon_pid)
968 wait_for_death(daemon_pid)
971 def test_preload_app_hup
972 hup_test_common(true)
976 hup_test_common(false)
979 def test_check_client_hup
980 hup_test_common(false, true)
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)
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)
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
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")
1036 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
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
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")
1052 fp.flock(File::LOCK_EX)
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}"
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"
1066 File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1068 rescue Errno::EEXIST
1072 File.unlink(lock_path) if lock_path
1075 fp.flock(File::LOCK_UN)