test_exec: drop sd_listen_fds emulation test
[unicorn.git] / test / exec / test_exec.rb
blob1d3a0fd62a513cba56e3773ddfca59db4177e678
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_WORKERS = 2
49   HEAVY_CFG = <<-EOS
50 worker_processes #{HEAVY_WORKERS}
51 timeout 30
52 logger Logger.new('#{COMMON_TMP.path}')
53 before_fork do |server, worker|
54   server.logger.info "before_fork: worker=\#{worker.nr}"
55 end
56   EOS
58   WORKING_DIRECTORY_CHECK_RU = <<-EOS
59 use Rack::ContentLength
60 run lambda { |env|
61   pwd = ENV['PWD']
62   a = ::File.stat(pwd)
63   b = ::File.stat(Dir.pwd)
64   if (a.ino == b.ino && a.dev == b.dev)
65     [ 200, { 'content-type' => 'text/plain' }, [ pwd ] ]
66   else
67     [ 404, { 'content-type' => 'text/plain' }, [] ]
68   end
70   EOS
72   def setup
73     @pwd = Dir.pwd
74     @tmpfile = Tempfile.new('unicorn_exec_test')
75     @tmpdir = @tmpfile.path
76     @tmpfile.close!
77     Dir.mkdir(@tmpdir)
78     Dir.chdir(@tmpdir)
79     @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
80     @port = unused_port(@addr)
81     @sockets = []
82     @start_pid = $$
83   end
85   def teardown
86     return if @start_pid != $$
87     Dir.chdir(@pwd)
88     FileUtils.rmtree(@tmpdir)
89     @sockets.each { |path| File.unlink(path) rescue nil }
90     loop do
91       Process.kill('-QUIT', 0)
92       begin
93         Process.waitpid(-1, Process::WNOHANG) or break
94       rescue Errno::ECHILD
95         break
96       end
97     end
98   end
100   def test_inherit_listener_unspecified
101     File.open("config.ru", "wb") { |fp| fp.write(HI) }
102     sock = TCPServer.new(@addr, @port)
103     sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
105     pid = xfork do
106       redirect_test_io do
107         ENV['UNICORN_FD'] = sock.fileno.to_s
108         exec($unicorn_bin, sock.fileno => sock.fileno)
109       end
110     end
111     res = hit(["http://#@addr:#@port/"])
112     assert_equal [ "HI\n" ], res
113     assert_shutdown(pid)
114     assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
115                 'unicorn should always set SO_KEEPALIVE on inherited sockets'
116   ensure
117     sock.close if sock
118   end
120   def test_working_directory_rel_path_config_file
121     other = Tempfile.new('unicorn.wd')
122     File.unlink(other.path)
123     Dir.mkdir(other.path)
124     File.open("config.ru", "wb") do |fp|
125       fp.syswrite WORKING_DIRECTORY_CHECK_RU
126     end
127     FileUtils.cp("config.ru", other.path + "/config.ru")
128     Dir.chdir(@tmpdir)
130     tmp = File.open('unicorn.config', 'wb')
131     tmp.syswrite <<EOF
132 working_directory '#@tmpdir'
133 listen '#@addr:#@port'
135     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
136     wait_workers_ready("test_stderr.#{pid}.log", 1)
137     results = hit(["http://#@addr:#@port/"])
138     assert_equal @tmpdir, results.first
139     File.truncate("test_stderr.#{pid}.log", 0)
141     tmp.sysseek(0)
142     tmp.truncate(0)
143     tmp.syswrite <<EOF
144 working_directory '#{other.path}'
145 listen '#@addr:#@port'
148     Process.kill(:HUP, pid)
149     lines = []
150     re = /config_file=(.+) would not be accessible in working_directory=(.+)/
151     until lines.grep(re)
152       sleep 0.1
153       lines = File.readlines("test_stderr.#{pid}.log")
154     end
156     File.truncate("test_stderr.#{pid}.log", 0)
157     FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
158     Process.kill(:HUP, pid)
159     wait_workers_ready("test_stderr.#{pid}.log", 1)
160     results = hit(["http://#@addr:#@port/"])
161     assert_equal other.path, results.first
163     Process.kill(:QUIT, pid)
164   ensure
165     FileUtils.rmtree(other.path)
166   end
168   def test_working_directory
169     other = Tempfile.new('unicorn.wd')
170     File.unlink(other.path)
171     Dir.mkdir(other.path)
172     File.open("config.ru", "wb") do |fp|
173       fp.syswrite WORKING_DIRECTORY_CHECK_RU
174     end
175     FileUtils.cp("config.ru", other.path + "/config.ru")
176     tmp = Tempfile.new('unicorn.config')
177     tmp.syswrite <<EOF
178 working_directory '#@tmpdir'
179 listen '#@addr:#@port'
181     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
182     wait_workers_ready("test_stderr.#{pid}.log", 1)
183     results = hit(["http://#@addr:#@port/"])
184     assert_equal @tmpdir, results.first
185     File.truncate("test_stderr.#{pid}.log", 0)
187     tmp.sysseek(0)
188     tmp.truncate(0)
189     tmp.syswrite <<EOF
190 working_directory '#{other.path}'
191 listen '#@addr:#@port'
194     Process.kill(:HUP, pid)
195     wait_workers_ready("test_stderr.#{pid}.log", 1)
196     results = hit(["http://#@addr:#@port/"])
197     assert_equal other.path, results.first
199     Process.kill(:QUIT, pid)
200   ensure
201     FileUtils.rmtree(other.path)
202   end
204   def test_working_directory_controls_relative_paths
205     other = Tempfile.new('unicorn.wd')
206     File.unlink(other.path)
207     Dir.mkdir(other.path)
208     File.open("config.ru", "wb") do |fp|
209       fp.syswrite WORKING_DIRECTORY_CHECK_RU
210     end
211     FileUtils.cp("config.ru", other.path + "/config.ru")
212     system('mkfifo', "#{other.path}/fifo")
213     tmp = Tempfile.new('unicorn.config')
214     tmp.syswrite <<EOF
215 pid "pid_file_here"
216 stderr_path "stderr_log_here"
217 stdout_path "stdout_log_here"
218 working_directory '#{other.path}'
219 listen '#@addr:#@port'
220 after_fork do |server, worker|
221   File.open("fifo", "wb").close
224     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
225     begin
226       fifo = File.open("#{other.path}/fifo", "rb")
227     rescue Errno::EINTR
228       # OpenBSD raises Errno::EINTR when opening
229       return if RUBY_PLATFORM =~ /openbsd/
230     end
231     fifo.close
233     assert ! File.exist?("stderr_log_here")
234     assert ! File.exist?("stdout_log_here")
235     assert ! File.exist?("pid_file_here")
237     assert ! File.exist?("#@tmpdir/stderr_log_here")
238     assert ! File.exist?("#@tmpdir/stdout_log_here")
239     assert ! File.exist?("#@tmpdir/pid_file_here")
241     assert File.exist?("#{other.path}/pid_file_here")
242     assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
243     assert File.exist?("#{other.path}/stderr_log_here")
244     assert File.exist?("#{other.path}/stdout_log_here")
245     wait_master_ready("#{other.path}/stderr_log_here")
247     Process.kill(:QUIT, pid)
248   ensure
249     FileUtils.rmtree(other.path)
250   end
252   def test_exit_signals
253     %w(INT TERM QUIT).each do |sig|
254       File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
255       pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
256       wait_master_ready("test_stderr.#{pid}.log")
257       wait_workers_ready("test_stderr.#{pid}.log", 1)
259       Process.kill(sig, pid)
260       pid, status = Process.waitpid2(pid)
262       reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
263       assert_equal 1, reaped.size
264       assert status.exited?
265     end
266   end
268   def test_basic
269     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
270     pid = fork do
271       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
272     end
273     results = retry_hit(["http://#{@addr}:#{@port}/"])
274     assert_equal String, results[0].class
275     assert_shutdown(pid)
276   end
278   def test_rack_env_unset
279     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
280     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
281     results = retry_hit(["http://#{@addr}:#{@port}/"])
282     assert_equal "development", results.first
283     assert_shutdown(pid)
284   end
286   def test_rack_env_cli_set
287     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
288     pid = fork {
289       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
290     }
291     results = retry_hit(["http://#{@addr}:#{@port}/"])
292     assert_equal "asdf", results.first
293     assert_shutdown(pid)
294   end
296   def test_rack_env_ENV_set
297     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
298     pid = fork {
299       ENV["RACK_ENV"] = "foobar"
300       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
301     }
302     results = retry_hit(["http://#{@addr}:#{@port}/"])
303     assert_equal "foobar", results.first
304     assert_shutdown(pid)
305   end
307   def test_rack_env_cli_override_ENV
308     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
309     pid = fork {
310       ENV["RACK_ENV"] = "foobar"
311       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
312     }
313     results = retry_hit(["http://#{@addr}:#{@port}/"])
314     assert_equal "asdf", results.first
315     assert_shutdown(pid)
316   end
318   def test_ttin_ttou
319     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
320     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
321     log = "test_stderr.#{pid}.log"
322     wait_master_ready(log)
323     [ 2, 3].each { |i|
324       Process.kill(:TTIN, pid)
325       wait_workers_ready(log, i)
326     }
327     File.truncate(log, 0)
328     reaped = nil
329     [ 2, 1, 0].each { |i|
330       Process.kill(:TTOU, pid)
331       DEFAULT_TRIES.times {
332         sleep DEFAULT_RES
333         reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
334         break if reaped.size == 1
335       }
336       assert_equal 1, reaped.size
337     }
338   end
340   def test_help
341     redirect_test_io do
342       assert(system($unicorn_bin, "-h"), "help text returns true")
343     end
344     assert_equal 0, File.stat("test_stderr.#$$.log").size
345     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
346     lines = File.readlines("test_stdout.#$$.log")
348     # Be considerate of the on-call technician working from their
349     # mobile phone or netbook on a slow connection :)
350     assert lines.size <= 24, "help height fits in an ANSI terminal window"
351     lines.each do |line|
352       line.chomp!
353       assert line.size <= 80, "help width fits in an ANSI terminal window"
354     end
355   end
357   def test_broken_reexec_config
358     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
359     pid_file = "#{@tmpdir}/test.pid"
360     old_file = "#{pid_file}.oldbin"
361     ucfg = Tempfile.new('unicorn_test_config')
362     ucfg.syswrite("listen %(#@addr:#@port)\n")
363     ucfg.syswrite("pid %(#{pid_file})\n")
364     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
365     pid = xfork do
366       redirect_test_io do
367         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
368       end
369     end
370     results = retry_hit(["http://#{@addr}:#{@port}/"])
371     assert_equal String, results[0].class
373     wait_for_file(pid_file)
374     Process.waitpid(pid)
375     Process.kill(:USR2, File.read(pid_file).to_i)
376     wait_for_file(old_file)
377     wait_for_file(pid_file)
378     old_pid = File.read(old_file).to_i
379     Process.kill(:QUIT, old_pid)
380     wait_for_death(old_pid)
382     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
383     current_pid = File.read(pid_file).to_i
384     Process.kill(:USR2, current_pid)
386     # wait for pid_file to restore itself
387     tries = DEFAULT_TRIES
388     begin
389       while current_pid != File.read(pid_file).to_i
390         sleep(DEFAULT_RES) and (tries -= 1) > 0
391       end
392     rescue Errno::ENOENT
393       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
394     end
395     assert_equal current_pid, File.read(pid_file).to_i
397     tries = DEFAULT_TRIES
398     while File.exist?(old_file)
399       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
400     end
401     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
402     port2 = unused_port(@addr)
404     # fix the bug
405     ucfg.sysseek(0)
406     ucfg.truncate(0)
407     ucfg.syswrite("listen %(#@addr:#@port)\n")
408     ucfg.syswrite("listen %(#@addr:#{port2})\n")
409     ucfg.syswrite("pid %(#{pid_file})\n")
410     Process.kill(:USR2, current_pid)
412     wait_for_file(old_file)
413     wait_for_file(pid_file)
414     new_pid = File.read(pid_file).to_i
415     assert_not_equal current_pid, new_pid
416     assert_equal current_pid, File.read(old_file).to_i
417     results = retry_hit(["http://#{@addr}:#{@port}/",
418                          "http://#{@addr}:#{port2}/"])
419     assert_equal String, results[0].class
420     assert_equal String, results[1].class
422     Process.kill(:QUIT, current_pid)
423     Process.kill(:QUIT, new_pid)
424   end
426   def test_broken_reexec_ru
427     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
428     pid_file = "#{@tmpdir}/test.pid"
429     old_file = "#{pid_file}.oldbin"
430     ucfg = Tempfile.new('unicorn_test_config')
431     ucfg.syswrite("pid %(#{pid_file})\n")
432     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
433     pid = xfork do
434       redirect_test_io do
435         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
436       end
437     end
438     results = retry_hit(["http://#{@addr}:#{@port}/"])
439     assert_equal String, results[0].class
441     wait_for_file(pid_file)
442     Process.waitpid(pid)
443     Process.kill(:USR2, File.read(pid_file).to_i)
444     wait_for_file(old_file)
445     wait_for_file(pid_file)
446     old_pid = File.read(old_file).to_i
447     Process.kill(:QUIT, old_pid)
448     wait_for_death(old_pid)
450     File.unlink("config.ru") # break reloading
451     current_pid = File.read(pid_file).to_i
452     Process.kill(:USR2, current_pid)
454     # wait for pid_file to restore itself
455     tries = DEFAULT_TRIES
456     begin
457       while current_pid != File.read(pid_file).to_i
458         sleep(DEFAULT_RES) and (tries -= 1) > 0
459       end
460     rescue Errno::ENOENT
461       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
462     end
464     tries = DEFAULT_TRIES
465     while File.exist?(old_file)
466       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
467     end
468     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
469     assert_equal current_pid, File.read(pid_file).to_i
471     # fix the bug
472     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
473     Process.kill(:USR2, current_pid)
474     wait_for_file(old_file)
475     wait_for_file(pid_file)
476     new_pid = File.read(pid_file).to_i
477     assert_not_equal current_pid, new_pid
478     assert_equal current_pid, File.read(old_file).to_i
479     results = retry_hit(["http://#{@addr}:#{@port}/"])
480     assert_equal String, results[0].class
482     Process.kill(:QUIT, current_pid)
483     Process.kill(:QUIT, new_pid)
484   end
486   def test_unicorn_config_listener_swap
487     port_cli = unused_port
488     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
489     ucfg = Tempfile.new('unicorn_test_config')
490     ucfg.syswrite("listen '#@addr:#@port'\n")
491     pid = xfork do
492       redirect_test_io do
493         exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
494       end
495     end
496     results = retry_hit(["http://#@addr:#{port_cli}/"])
497     assert_equal String, results[0].class
498     results = retry_hit(["http://#@addr:#@port/"])
499     assert_equal String, results[0].class
501     port2 = unused_port(@addr)
502     ucfg.sysseek(0)
503     ucfg.truncate(0)
504     ucfg.syswrite("listen '#@addr:#{port2}'\n")
505     Process.kill(:HUP, pid)
507     results = retry_hit(["http://#@addr:#{port2}/"])
508     assert_equal String, results[0].class
509     results = retry_hit(["http://#@addr:#{port_cli}/"])
510     assert_equal String, results[0].class
511     reuse = TCPServer.new(@addr, @port)
512     reuse.close
513     assert_shutdown(pid)
514   end
516   def test_unicorn_config_listen_with_options
517     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
518     ucfg = Tempfile.new('unicorn_test_config')
519     ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
520     ucfg.syswrite("                            :rcvbuf => 4096,\n")
521     ucfg.syswrite("                            :sndbuf => 4096\n")
522     pid = xfork do
523       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
524     end
525     results = retry_hit(["http://#{@addr}:#{@port}/"])
526     assert_equal String, results[0].class
527     assert_shutdown(pid)
528   end
530   def test_unicorn_config_per_worker_listen
531     port2 = unused_port
532     pid_spit = 'use Rack::ContentLength;' \
533       'run proc { |e| [ 200, {"content-type"=>"text/plain"}, ["#$$\\n"] ] }'
534     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
535     tmp = Tempfile.new('test.socket')
536     File.unlink(tmp.path)
537     ucfg = Tempfile.new('unicorn_test_config')
538     ucfg.syswrite("listen '#@addr:#@port'\n")
539     ucfg.syswrite("after_fork { |s,w|\n")
540     ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
541     ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
542     ucfg.syswrite("\n}\n")
543     pid = xfork do
544       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
545     end
546     results = retry_hit(["http://#{@addr}:#{@port}/"])
547     assert_equal String, results[0].class
548     worker_pid = results[0].to_i
549     assert_not_equal pid, worker_pid
550     s = unix_socket(tmp.path)
551     s.syswrite("GET / HTTP/1.0\r\n\r\n")
552     results = ''
553     loop { results << s.sysread(4096) } rescue nil
554     s.close
555     assert_equal worker_pid, results.split(/\r\n/).last.to_i
556     results = hit(["http://#@addr:#{port2}/"])
557     assert_equal String, results[0].class
558     assert_equal worker_pid, results[0].to_i
559     assert_shutdown(pid)
560   end
562   def test_unicorn_config_listen_augments_cli
563     port2 = unused_port(@addr)
564     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
565     ucfg = Tempfile.new('unicorn_test_config')
566     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
567     pid = xfork do
568       redirect_test_io do
569         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
570       end
571     end
572     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
573     results = retry_hit(uris)
574     assert_equal results.size, uris.size
575     assert_equal String, results[0].class
576     assert_equal String, results[1].class
577     assert_shutdown(pid)
578   end
580   def test_weird_config_settings
581     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
582     ucfg = Tempfile.new('unicorn_test_config')
583     proc_total = HEAVY_WORKERS + 1 # + 1 for master
584     ucfg.syswrite(HEAVY_CFG)
585     pid = xfork do
586       redirect_test_io do
587         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
588       end
589     end
591     results = retry_hit(["http://#{@addr}:#{@port}/"])
592     assert_equal String, results[0].class
593     wait_master_ready(COMMON_TMP.path)
594     wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS)
595     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
596     assert_equal HEAVY_WORKERS, bf.size
597     rotate = Tempfile.new('unicorn_rotate')
599     File.rename(COMMON_TMP.path, rotate.path)
600     Process.kill(:USR1, pid)
602     wait_for_file(COMMON_TMP.path)
603     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
604     # USR1 should've been passed to all workers
605     tries = DEFAULT_TRIES
606     log = File.readlines(rotate.path)
607     while (tries -= 1) > 0 &&
608           log.grep(/reopening logs\.\.\./).size < proc_total
609       sleep DEFAULT_RES
610       log = File.readlines(rotate.path)
611     end
612     assert_equal proc_total, log.grep(/reopening logs\.\.\./).size
613     assert_equal 0, log.grep(/done reopening logs/).size
615     tries = DEFAULT_TRIES
616     log = File.readlines(COMMON_TMP.path)
617     while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total
618       sleep DEFAULT_RES
619       log = File.readlines(COMMON_TMP.path)
620     end
621     assert_equal proc_total, log.grep(/done reopening logs/).size
622     assert_equal 0, log.grep(/reopening logs\.\.\./).size
624     Process.kill(:QUIT, pid)
625     pid, status = Process.waitpid2(pid)
627     assert status.success?, "exited successfully"
628   end
630   def test_read_embedded_cli_switches
631     File.open("config.ru", "wb") do |fp|
632       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
633       fp.syswrite(HI)
634     end
635     pid = fork { redirect_test_io { exec($unicorn_bin) } }
636     results = retry_hit(["http://#{@addr}:#{@port}/"])
637     assert_equal String, results[0].class
638     assert_shutdown(pid)
639   end
641   def test_config_ru_alt_path
642     config_path = "#{@tmpdir}/foo.ru"
643     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
644     pid = fork do
645       redirect_test_io do
646         Dir.chdir("/")
647         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
648       end
649     end
650     results = retry_hit(["http://#{@addr}:#{@port}/"])
651     assert_equal String, results[0].class
652     assert_shutdown(pid)
653   end
655   def test_load_module
656     libdir = "#{@tmpdir}/lib"
657     FileUtils.mkpath([ libdir ])
658     config_path = "#{libdir}/hello.rb"
659     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
660     pid = fork do
661       redirect_test_io do
662         Dir.chdir("/")
663         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
664       end
665     end
666     results = retry_hit(["http://#{@addr}:#{@port}/"])
667     assert_equal String, results[0].class
668     assert_shutdown(pid)
669   end
671   def test_reexec
672     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
673     pid_file = "#{@tmpdir}/test.pid"
674     pid = fork do
675       redirect_test_io do
676         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
677       end
678     end
679     reexec_basic_test(pid, pid_file)
680   end
682   def test_reexec_alt_config
683     config_file = "#{@tmpdir}/foo.ru"
684     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
685     pid_file = "#{@tmpdir}/test.pid"
686     pid = fork do
687       redirect_test_io do
688         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
689       end
690     end
691     reexec_basic_test(pid, pid_file)
692   end
694   def test_socket_unlinked_restore
695     results = nil
696     sock = Tempfile.new('unicorn_test_sock')
697     sock_path = sock.path
698     @sockets << sock_path
699     sock.close!
700     ucfg = Tempfile.new('unicorn_test_config')
701     ucfg.syswrite("listen \"#{sock_path}\"\n")
703     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
704     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
705     wait_for_file(sock_path)
706     assert File.socket?(sock_path)
708     sock = unix_socket(sock_path)
709     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
710     results = sock.sysread(4096)
712     assert_equal String, results.class
713     File.unlink(sock_path)
714     Process.kill(:HUP, pid)
715     wait_for_file(sock_path)
716     assert File.socket?(sock_path)
718     sock = unix_socket(sock_path)
719     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
720     results = sock.sysread(4096)
722     assert_equal String, results.class
723   end
725   def test_unicorn_config_file
726     pid_file = "#{@tmpdir}/test.pid"
727     sock = Tempfile.new('unicorn_test_sock')
728     sock_path = sock.path
729     sock.close!
730     @sockets << sock_path
732     log = Tempfile.new('unicorn_test_log')
733     ucfg = Tempfile.new('unicorn_test_config')
734     ucfg.syswrite("listen \"#{sock_path}\"\n")
735     ucfg.syswrite("pid \"#{pid_file}\"\n")
736     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
737     ucfg.close
739     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
740     pid = xfork do
741       redirect_test_io do
742         exec($unicorn_bin, "-l#{@addr}:#{@port}",
743              "-P#{pid_file}", "-c#{ucfg.path}")
744       end
745     end
746     results = retry_hit(["http://#{@addr}:#{@port}/"])
747     assert_equal String, results[0].class
748     wait_master_ready(log.path)
749     assert File.exist?(pid_file), "pid_file created"
750     assert_equal pid, File.read(pid_file).to_i
751     assert File.socket?(sock_path), "socket created"
753     sock = unix_socket(sock_path)
754     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
755     results = sock.sysread(4096)
757     assert_equal String, results.class
759     # try reloading the config
760     sock = Tempfile.new('new_test_sock')
761     new_sock_path = sock.path
762     @sockets << new_sock_path
763     sock.close!
764     new_log = Tempfile.new('unicorn_test_log')
765     new_log.sync = true
766     assert_equal 0, new_log.size
768     ucfg = File.open(ucfg.path, "wb")
769     ucfg.syswrite("listen \"#{sock_path}\"\n")
770     ucfg.syswrite("listen \"#{new_sock_path}\"\n")
771     ucfg.syswrite("pid \"#{pid_file}\"\n")
772     ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
773     ucfg.close
774     Process.kill(:HUP, pid)
776     wait_for_file(new_sock_path)
777     assert File.socket?(new_sock_path), "socket exists"
778     @sockets.each do |path|
779       sock = unix_socket(path)
780       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
781       results = sock.sysread(4096)
782       assert_equal String, results.class
783     end
785     assert_not_equal 0, new_log.size
786     reexec_usr2_quit_test(pid, pid_file)
787   end
789   def test_daemonize_reexec
790     pid_file = "#{@tmpdir}/test.pid"
791     log = Tempfile.new('unicorn_test_log')
792     ucfg = Tempfile.new('unicorn_test_config')
793     ucfg.syswrite("pid \"#{pid_file}\"\n")
794     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
795     ucfg.close
797     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
798     pid = xfork do
799       redirect_test_io do
800         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
801       end
802     end
803     results = retry_hit(["http://#{@addr}:#{@port}/"])
804     assert_equal String, results[0].class
805     wait_for_file(pid_file)
806     new_pid = File.read(pid_file).to_i
807     assert_not_equal pid, new_pid
808     pid, status = Process.waitpid2(pid)
809     assert status.success?, "original process exited successfully"
810     Process.kill(0, new_pid)
811     reexec_usr2_quit_test(new_pid, pid_file)
812   end
814   def test_daemonize_redirect_fail
815     pid_file = "#{@tmpdir}/test.pid"
816     ucfg = Tempfile.new('unicorn_test_config')
817     ucfg.syswrite("pid #{pid_file}\"\n")
818     err = Tempfile.new('stderr')
819     out = Tempfile.new('stdout ')
821     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
822     pid = xfork do
823       $stderr.reopen(err.path, "a")
824       $stdout.reopen(out.path, "a")
825       exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
826     end
827     pid, status = Process.waitpid2(pid)
828     assert ! status.success?, "original process exited successfully"
829     sleep 1 # can't waitpid on a daemonized process :<
830     assert err.stat.size > 0
831   end
833   def test_reexec_fd_leak
834     unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
835       warn "FD leak test only works on Linux at the moment"
836       return
837     end
838     pid_file = "#{@tmpdir}/test.pid"
839     log = Tempfile.new('unicorn_test_log')
840     log.sync = true
841     ucfg = Tempfile.new('unicorn_test_config')
842     ucfg.syswrite("pid \"#{pid_file}\"\n")
843     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
844     ucfg.syswrite("stderr_path '#{log.path}'\n")
845     ucfg.syswrite("stdout_path '#{log.path}'\n")
846     ucfg.close
848     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
849     pid = xfork do
850       redirect_test_io do
851         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
852       end
853     end
855     wait_master_ready(log.path)
856     wait_workers_ready(log.path, 1)
857     File.truncate(log.path, 0)
858     wait_for_file(pid_file)
859     orig_pid = pid = File.read(pid_file).to_i
860     orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
861     assert $?.success?
862     expect_size = orig_fds.size
864     Process.kill(:USR2, pid)
865     wait_for_file("#{pid_file}.oldbin")
866     Process.kill(:QUIT, pid)
868     wait_for_death(pid)
870     wait_master_ready(log.path)
871     wait_workers_ready(log.path, 1)
872     File.truncate(log.path, 0)
873     wait_for_file(pid_file)
874     pid = File.read(pid_file).to_i
875     assert_not_equal orig_pid, pid
876     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
877     assert $?.success?
879     # we could've inherited descriptors the first time around
880     assert expect_size >= curr_fds.size, curr_fds.inspect
881     expect_size = curr_fds.size
883     Process.kill(:USR2, pid)
884     wait_for_file("#{pid_file}.oldbin")
885     Process.kill(:QUIT, pid)
887     wait_for_death(pid)
889     wait_master_ready(log.path)
890     wait_workers_ready(log.path, 1)
891     File.truncate(log.path, 0)
892     wait_for_file(pid_file)
893     pid = File.read(pid_file).to_i
894     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
895     assert $?.success?
896     assert_equal expect_size, curr_fds.size, curr_fds.inspect
898     Process.kill(:QUIT, pid)
899     wait_for_death(pid)
900   end
902   def hup_test_common(preload, check_client=false)
903     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
904     pid_file = Tempfile.new('pid')
905     ucfg = Tempfile.new('unicorn_test_config')
906     ucfg.syswrite("listen '#@addr:#@port'\n")
907     ucfg.syswrite("pid '#{pid_file.path}'\n")
908     ucfg.syswrite("preload_app true\n") if preload
909     ucfg.syswrite("check_client_connection true\n") if check_client
910     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
911     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
912     pid = xfork {
913       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
914     }
915     _, status = Process.waitpid2(pid)
916     assert status.success?
917     wait_master_ready("test_stderr.#$$.log")
918     wait_workers_ready("test_stderr.#$$.log", 1)
919     uri = URI.parse("http://#@addr:#@port/")
920     pids = Tempfile.new('worker_pids')
921     r, w = IO.pipe
922     hitter = fork {
923       r.close
924       bodies = Hash.new(0)
925       at_exit { pids.syswrite(bodies.inspect) }
926       trap(:TERM) { exit(0) }
927       nr = 0
928       loop {
929         rv = Net::HTTP.get(uri)
930         pid = rv.to_i
931         exit!(1) if pid <= 0
932         bodies[pid] += 1
933         nr += 1
934         if nr == 1
935           w.syswrite('1')
936         elsif bodies.size > 1
937           w.syswrite('2')
938           sleep
939         end
940       }
941     }
942     w.close
943     assert_equal '1', r.read(1)
944     daemon_pid = File.read(pid_file.path).to_i
945     assert daemon_pid > 0
946     Process.kill(:HUP, daemon_pid)
947     assert_equal '2', r.read(1)
948     Process.kill(:TERM, hitter)
949     _, hitter_status = Process.waitpid2(hitter)
950     assert(hitter_status.success?,
951            "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
952            "#{File.read("test_stderr.#$$.log")}")
953     pids.sysseek(0)
954     pids = eval(pids.read)
955     assert_kind_of(Hash, pids)
956     assert_equal 2, pids.size
957     pids.keys.each { |x|
958       assert_kind_of(Integer, x)
959       assert x > 0
960       assert pids[x] > 0
961     }
962     Process.kill(:QUIT, daemon_pid)
963     wait_for_death(daemon_pid)
964   end
966   def test_preload_app_hup
967     hup_test_common(true)
968   end
970   def test_hup
971     hup_test_common(false)
972   end
974   def test_check_client_hup
975     hup_test_common(false, true)
976   end
978   def test_default_listen_hup_holds_listener
979     default_listen_lock do
980       res, pid_path = default_listen_setup
981       daemon_pid = File.read(pid_path).to_i
982       Process.kill(:HUP, daemon_pid)
983       wait_workers_ready("test_stderr.#$$.log", 1)
984       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
985       assert_match %r{\d+}, res2.first
986       assert res2.first != res.first
987       Process.kill(:QUIT, daemon_pid)
988       wait_for_death(daemon_pid)
989     end
990   end
992   def test_default_listen_upgrade_holds_listener
993     default_listen_lock do
994       res, pid_path = default_listen_setup
995       daemon_pid = File.read(pid_path).to_i
997       Process.kill(:USR2, daemon_pid)
998       wait_for_file("#{pid_path}.oldbin")
999       wait_for_file(pid_path)
1000       Process.kill(:QUIT, daemon_pid)
1001       wait_for_death(daemon_pid)
1003       daemon_pid = File.read(pid_path).to_i
1004       wait_workers_ready("test_stderr.#$$.log", 1)
1005       File.truncate("test_stderr.#$$.log", 0)
1007       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1008       assert_match %r{\d+}, res2.first
1009       assert res2.first != res.first
1011       Process.kill(:HUP, daemon_pid)
1012       wait_workers_ready("test_stderr.#$$.log", 1)
1013       File.truncate("test_stderr.#$$.log", 0)
1014       res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1015       assert res2.first != res3.first
1017       Process.kill(:QUIT, daemon_pid)
1018       wait_for_death(daemon_pid)
1019     end
1020   end
1022   def default_listen_setup
1023     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
1024     pid_path = (tmp = Tempfile.new('pid')).path
1025     tmp.close!
1026     ucfg = Tempfile.new('unicorn_test_config')
1027     ucfg.syswrite("pid '#{pid_path}'\n")
1028     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1029     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1030     pid = xfork {
1031       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1032     }
1033     _, status = Process.waitpid2(pid)
1034     assert status.success?
1035     wait_master_ready("test_stderr.#$$.log")
1036     wait_workers_ready("test_stderr.#$$.log", 1)
1037     File.truncate("test_stderr.#$$.log", 0)
1038     res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1039     assert_match %r{\d+}, res.first
1040     [ res, pid_path ]
1041   end
1043   # we need to flock() something to prevent these tests from running
1044   def default_listen_lock(&block)
1045     fp = File.open(FLOCK_PATH, "rb")
1046     begin
1047       fp.flock(File::LOCK_EX)
1048       begin
1049         TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1050                       Unicorn::Const::DEFAULT_PORT).close
1051       rescue Errno::EADDRINUSE, Errno::EACCES
1052         warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1053         return false
1054       end
1056       # unused_port should never take this, but we may run an environment
1057       # where tests are being run against older unicorns...
1058       lock_path = "#{Dir::tmpdir}/unicorn_test." \
1059                   "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1060       begin
1061         File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1062         yield
1063       rescue Errno::EEXIST
1064         lock_path = nil
1065         return false
1066       ensure
1067         File.unlink(lock_path) if lock_path
1068       end
1069     ensure
1070       fp.flock(File::LOCK_UN)
1071     end
1072   end
1074 end if do_test