fix per-worker listen directive in after_fork hook
[unicorn.git] / test / exec / test_exec.rb
blob8c33457aafc895b078635dd859a72af52b7030a6
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_working_directory_rel_path_config_file
100     other = Tempfile.new('unicorn.wd')
101     File.unlink(other.path)
102     Dir.mkdir(other.path)
103     File.open("config.ru", "wb") do |fp|
104       fp.syswrite WORKING_DIRECTORY_CHECK_RU
105     end
106     FileUtils.cp("config.ru", other.path + "/config.ru")
107     Dir.chdir(@tmpdir)
109     tmp = File.open('unicorn.config', 'wb')
110     tmp.syswrite <<EOF
111 working_directory '#@tmpdir'
112 listen '#@addr:#@port'
114     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
115     wait_workers_ready("test_stderr.#{pid}.log", 1)
116     results = hit(["http://#@addr:#@port/"])
117     assert_equal @tmpdir, results.first
118     File.truncate("test_stderr.#{pid}.log", 0)
120     tmp.sysseek(0)
121     tmp.truncate(0)
122     tmp.syswrite <<EOF
123 working_directory '#{other.path}'
124 listen '#@addr:#@port'
127     Process.kill(:HUP, pid)
128     lines = []
129     re = /config_file=(.+) would not be accessible in working_directory=(.+)/
130     until lines.grep(re)
131       sleep 0.1
132       lines = File.readlines("test_stderr.#{pid}.log")
133     end
135     File.truncate("test_stderr.#{pid}.log", 0)
136     FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
137     Process.kill(:HUP, pid)
138     wait_workers_ready("test_stderr.#{pid}.log", 1)
139     results = hit(["http://#@addr:#@port/"])
140     assert_equal other.path, results.first
142     Process.kill(:QUIT, pid)
143     ensure
144       FileUtils.rmtree(other.path)
145   end
147   def test_working_directory
148     other = Tempfile.new('unicorn.wd')
149     File.unlink(other.path)
150     Dir.mkdir(other.path)
151     File.open("config.ru", "wb") do |fp|
152       fp.syswrite WORKING_DIRECTORY_CHECK_RU
153     end
154     FileUtils.cp("config.ru", other.path + "/config.ru")
155     tmp = Tempfile.new('unicorn.config')
156     tmp.syswrite <<EOF
157 working_directory '#@tmpdir'
158 listen '#@addr:#@port'
160     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
161     wait_workers_ready("test_stderr.#{pid}.log", 1)
162     results = hit(["http://#@addr:#@port/"])
163     assert_equal @tmpdir, results.first
164     File.truncate("test_stderr.#{pid}.log", 0)
166     tmp.sysseek(0)
167     tmp.truncate(0)
168     tmp.syswrite <<EOF
169 working_directory '#{other.path}'
170 listen '#@addr:#@port'
173     Process.kill(:HUP, pid)
174     wait_workers_ready("test_stderr.#{pid}.log", 1)
175     results = hit(["http://#@addr:#@port/"])
176     assert_equal other.path, results.first
178     Process.kill(:QUIT, pid)
179     ensure
180       FileUtils.rmtree(other.path)
181   end
183   def test_working_directory_controls_relative_paths
184     other = Tempfile.new('unicorn.wd')
185     File.unlink(other.path)
186     Dir.mkdir(other.path)
187     File.open("config.ru", "wb") do |fp|
188       fp.syswrite WORKING_DIRECTORY_CHECK_RU
189     end
190     FileUtils.cp("config.ru", other.path + "/config.ru")
191     system('mkfifo', "#{other.path}/fifo")
192     tmp = Tempfile.new('unicorn.config')
193     tmp.syswrite <<EOF
194 pid "pid_file_here"
195 stderr_path "stderr_log_here"
196 stdout_path "stdout_log_here"
197 working_directory '#{other.path}'
198 listen '#@addr:#@port'
199 after_fork do |server, worker|
200   File.open("fifo", "wb").close
203     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
204     File.open("#{other.path}/fifo", "rb").close
206     assert ! File.exist?("stderr_log_here")
207     assert ! File.exist?("stdout_log_here")
208     assert ! File.exist?("pid_file_here")
210     assert ! File.exist?("#@tmpdir/stderr_log_here")
211     assert ! File.exist?("#@tmpdir/stdout_log_here")
212     assert ! File.exist?("#@tmpdir/pid_file_here")
214     assert File.exist?("#{other.path}/pid_file_here")
215     assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
216     assert File.exist?("#{other.path}/stderr_log_here")
217     assert File.exist?("#{other.path}/stdout_log_here")
218     wait_master_ready("#{other.path}/stderr_log_here")
220     Process.kill(:QUIT, pid)
221     ensure
222       FileUtils.rmtree(other.path)
223   end
226   def test_exit_signals
227     %w(INT TERM QUIT).each do |sig|
228       File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
229       pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
230       wait_master_ready("test_stderr.#{pid}.log")
231       wait_workers_ready("test_stderr.#{pid}.log", 1)
232       status = nil
233       assert_nothing_raised do
234         Process.kill(sig, pid)
235         pid, status = Process.waitpid2(pid)
236       end
237       reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
238       assert_equal 1, reaped.size
239       assert status.exited?
240     end
241   end
243   def test_basic
244     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
245     pid = fork do
246       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
247     end
248     results = retry_hit(["http://#{@addr}:#{@port}/"])
249     assert_equal String, results[0].class
250     assert_shutdown(pid)
251   end
253   def test_rack_env_unset
254     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
255     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
256     results = retry_hit(["http://#{@addr}:#{@port}/"])
257     assert_equal "development", results.first
258     assert_shutdown(pid)
259   end
261   def test_rack_env_cli_set
262     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
263     pid = fork {
264       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
265     }
266     results = retry_hit(["http://#{@addr}:#{@port}/"])
267     assert_equal "asdf", results.first
268     assert_shutdown(pid)
269   end
271   def test_rack_env_ENV_set
272     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
273     pid = fork {
274       ENV["RACK_ENV"] = "foobar"
275       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
276     }
277     results = retry_hit(["http://#{@addr}:#{@port}/"])
278     assert_equal "foobar", results.first
279     assert_shutdown(pid)
280   end
282   def test_rack_env_cli_override_ENV
283     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
284     pid = fork {
285       ENV["RACK_ENV"] = "foobar"
286       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
287     }
288     results = retry_hit(["http://#{@addr}:#{@port}/"])
289     assert_equal "asdf", results.first
290     assert_shutdown(pid)
291   end
293   def test_ttin_ttou
294     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
295     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
296     log = "test_stderr.#{pid}.log"
297     wait_master_ready(log)
298     [ 2, 3].each { |i|
299       assert_nothing_raised { Process.kill(:TTIN, pid) }
300       wait_workers_ready(log, i)
301     }
302     File.truncate(log, 0)
303     reaped = nil
304     [ 2, 1, 0].each { |i|
305       assert_nothing_raised { Process.kill(:TTOU, pid) }
306       DEFAULT_TRIES.times {
307         sleep DEFAULT_RES
308         reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
309         break if reaped.size == 1
310       }
311       assert_equal 1, reaped.size
312     }
313   end
315   def test_help
316     redirect_test_io do
317       assert(system($unicorn_bin, "-h"), "help text returns true")
318     end
319     assert_equal 0, File.stat("test_stderr.#$$.log").size
320     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
321     lines = File.readlines("test_stdout.#$$.log")
323     # Be considerate of the on-call technician working from their
324     # mobile phone or netbook on a slow connection :)
325     assert lines.size <= 24, "help height fits in an ANSI terminal window"
326     lines.each do |line|
327       assert line.size <= 80, "help width fits in an ANSI terminal window"
328     end
329   end
331   def test_broken_reexec_config
332     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
333     pid_file = "#{@tmpdir}/test.pid"
334     old_file = "#{pid_file}.oldbin"
335     ucfg = Tempfile.new('unicorn_test_config')
336     ucfg.syswrite("listen %(#@addr:#@port)\n")
337     ucfg.syswrite("pid %(#{pid_file})\n")
338     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
339     pid = xfork do
340       redirect_test_io do
341         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
342       end
343     end
344     results = retry_hit(["http://#{@addr}:#{@port}/"])
345     assert_equal String, results[0].class
347     wait_for_file(pid_file)
348     Process.waitpid(pid)
349     Process.kill(:USR2, File.read(pid_file).to_i)
350     wait_for_file(old_file)
351     wait_for_file(pid_file)
352     old_pid = File.read(old_file).to_i
353     Process.kill(:QUIT, old_pid)
354     wait_for_death(old_pid)
356     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
357     current_pid = File.read(pid_file).to_i
358     Process.kill(:USR2, current_pid)
360     # wait for pid_file to restore itself
361     tries = DEFAULT_TRIES
362     begin
363       while current_pid != File.read(pid_file).to_i
364         sleep(DEFAULT_RES) and (tries -= 1) > 0
365       end
366     rescue Errno::ENOENT
367       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
368     end
369     assert_equal current_pid, File.read(pid_file).to_i
371     tries = DEFAULT_TRIES
372     while File.exist?(old_file)
373       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
374     end
375     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
376     port2 = unused_port(@addr)
378     # fix the bug
379     ucfg.sysseek(0)
380     ucfg.truncate(0)
381     ucfg.syswrite("listen %(#@addr:#@port)\n")
382     ucfg.syswrite("listen %(#@addr:#{port2})\n")
383     ucfg.syswrite("pid %(#{pid_file})\n")
384     assert_nothing_raised { Process.kill(:USR2, current_pid) }
386     wait_for_file(old_file)
387     wait_for_file(pid_file)
388     new_pid = File.read(pid_file).to_i
389     assert_not_equal current_pid, new_pid
390     assert_equal current_pid, File.read(old_file).to_i
391     results = retry_hit(["http://#{@addr}:#{@port}/",
392                          "http://#{@addr}:#{port2}/"])
393     assert_equal String, results[0].class
394     assert_equal String, results[1].class
396     assert_nothing_raised do
397       Process.kill(:QUIT, current_pid)
398       Process.kill(:QUIT, new_pid)
399     end
400   end
402   def test_broken_reexec_ru
403     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
404     pid_file = "#{@tmpdir}/test.pid"
405     old_file = "#{pid_file}.oldbin"
406     ucfg = Tempfile.new('unicorn_test_config')
407     ucfg.syswrite("pid %(#{pid_file})\n")
408     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
409     pid = xfork do
410       redirect_test_io do
411         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
412       end
413     end
414     results = retry_hit(["http://#{@addr}:#{@port}/"])
415     assert_equal String, results[0].class
417     wait_for_file(pid_file)
418     Process.waitpid(pid)
419     Process.kill(:USR2, File.read(pid_file).to_i)
420     wait_for_file(old_file)
421     wait_for_file(pid_file)
422     old_pid = File.read(old_file).to_i
423     Process.kill(:QUIT, old_pid)
424     wait_for_death(old_pid)
426     File.unlink("config.ru") # break reloading
427     current_pid = File.read(pid_file).to_i
428     Process.kill(:USR2, current_pid)
430     # wait for pid_file to restore itself
431     tries = DEFAULT_TRIES
432     begin
433       while current_pid != File.read(pid_file).to_i
434         sleep(DEFAULT_RES) and (tries -= 1) > 0
435       end
436     rescue Errno::ENOENT
437       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
438     end
440     tries = DEFAULT_TRIES
441     while File.exist?(old_file)
442       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
443     end
444     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
445     assert_equal current_pid, File.read(pid_file).to_i
447     # fix the bug
448     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
449     assert_nothing_raised { Process.kill(:USR2, current_pid) }
450     wait_for_file(old_file)
451     wait_for_file(pid_file)
452     new_pid = File.read(pid_file).to_i
453     assert_not_equal current_pid, new_pid
454     assert_equal current_pid, File.read(old_file).to_i
455     results = retry_hit(["http://#{@addr}:#{@port}/"])
456     assert_equal String, results[0].class
458     assert_nothing_raised do
459       Process.kill(:QUIT, current_pid)
460       Process.kill(:QUIT, new_pid)
461     end
462   end
464   def test_unicorn_config_listener_swap
465     port_cli = unused_port
466     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
467     ucfg = Tempfile.new('unicorn_test_config')
468     ucfg.syswrite("listen '#@addr:#@port'\n")
469     pid = xfork do
470       redirect_test_io do
471         exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
472       end
473     end
474     results = retry_hit(["http://#@addr:#{port_cli}/"])
475     assert_equal String, results[0].class
476     results = retry_hit(["http://#@addr:#@port/"])
477     assert_equal String, results[0].class
479     port2 = unused_port(@addr)
480     ucfg.sysseek(0)
481     ucfg.truncate(0)
482     ucfg.syswrite("listen '#@addr:#{port2}'\n")
483     Process.kill(:HUP, pid)
485     results = retry_hit(["http://#@addr:#{port2}/"])
486     assert_equal String, results[0].class
487     results = retry_hit(["http://#@addr:#{port_cli}/"])
488     assert_equal String, results[0].class
489     assert_nothing_raised do
490       reuse = TCPServer.new(@addr, @port)
491       reuse.close
492     end
493     assert_shutdown(pid)
494   end
496   def test_unicorn_config_listen_with_options
497     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
498     ucfg = Tempfile.new('unicorn_test_config')
499     ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
500     ucfg.syswrite("                            :rcvbuf => 4096,\n")
501     ucfg.syswrite("                            :sndbuf => 4096\n")
502     pid = xfork do
503       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
504     end
505     results = retry_hit(["http://#{@addr}:#{@port}/"])
506     assert_equal String, results[0].class
507     assert_shutdown(pid)
508   end
510   def test_unicorn_config_per_worker_listen
511     port2 = unused_port
512     pid_spit = 'use Rack::ContentLength;' \
513       'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
514     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
515     tmp = Tempfile.new('test.socket')
516     File.unlink(tmp.path)
517     ucfg = Tempfile.new('unicorn_test_config')
518     ucfg.syswrite("listen '#@addr:#@port'\n")
519     ucfg.syswrite("after_fork { |s,w|\n")
520     ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
521     ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
522     ucfg.syswrite("\n}\n")
523     pid = xfork do
524       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
525     end
526     results = retry_hit(["http://#{@addr}:#{@port}/"])
527     assert_equal String, results[0].class
528     worker_pid = results[0].to_i
529     assert_not_equal pid, worker_pid
530     s = UNIXSocket.new(tmp.path)
531     s.syswrite("GET / HTTP/1.0\r\n\r\n")
532     results = ''
533     loop { results << s.sysread(4096) } rescue nil
534     assert_nothing_raised { s.close }
535     assert_equal worker_pid, results.split(/\r\n/).last.to_i
536     results = hit(["http://#@addr:#{port2}/"])
537     assert_equal String, results[0].class
538     assert_equal worker_pid, results[0].to_i
539     assert_shutdown(pid)
540   end
542   def test_unicorn_config_listen_augments_cli
543     port2 = unused_port(@addr)
544     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
545     ucfg = Tempfile.new('unicorn_test_config')
546     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
547     pid = xfork do
548       redirect_test_io do
549         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
550       end
551     end
552     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
553     results = retry_hit(uris)
554     assert_equal results.size, uris.size
555     assert_equal String, results[0].class
556     assert_equal String, results[1].class
557     assert_shutdown(pid)
558   end
560   def test_weird_config_settings
561     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
562     ucfg = Tempfile.new('unicorn_test_config')
563     ucfg.syswrite(HEAVY_CFG)
564     pid = xfork do
565       redirect_test_io do
566         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
567       end
568     end
570     results = retry_hit(["http://#{@addr}:#{@port}/"])
571     assert_equal String, results[0].class
572     wait_master_ready(COMMON_TMP.path)
573     wait_workers_ready(COMMON_TMP.path, 4)
574     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
575     assert_equal 4, bf.size
576     rotate = Tempfile.new('unicorn_rotate')
577     assert_nothing_raised do
578       File.rename(COMMON_TMP.path, rotate.path)
579       Process.kill(:USR1, pid)
580     end
581     wait_for_file(COMMON_TMP.path)
582     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
583     # USR1 should've been passed to all workers
584     tries = DEFAULT_TRIES
585     log = File.readlines(rotate.path)
586     while (tries -= 1) > 0 &&
587           log.grep(/reopening logs\.\.\./).size < 5
588       sleep DEFAULT_RES
589       log = File.readlines(rotate.path)
590     end
591     assert_equal 5, log.grep(/reopening logs\.\.\./).size
592     assert_equal 0, log.grep(/done reopening logs/).size
594     tries = DEFAULT_TRIES
595     log = File.readlines(COMMON_TMP.path)
596     while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
597       sleep DEFAULT_RES
598       log = File.readlines(COMMON_TMP.path)
599     end
600     assert_equal 5, log.grep(/done reopening logs/).size
601     assert_equal 0, log.grep(/reopening logs\.\.\./).size
602     assert_nothing_raised { Process.kill(:QUIT, pid) }
603     status = nil
604     assert_nothing_raised { pid, status = Process.waitpid2(pid) }
605     assert status.success?, "exited successfully"
606   end
608   def test_read_embedded_cli_switches
609     File.open("config.ru", "wb") do |fp|
610       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
611       fp.syswrite(HI)
612     end
613     pid = fork { redirect_test_io { exec($unicorn_bin) } }
614     results = retry_hit(["http://#{@addr}:#{@port}/"])
615     assert_equal String, results[0].class
616     assert_shutdown(pid)
617   end
619   def test_config_ru_alt_path
620     config_path = "#{@tmpdir}/foo.ru"
621     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
622     pid = fork do
623       redirect_test_io do
624         Dir.chdir("/")
625         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
626       end
627     end
628     results = retry_hit(["http://#{@addr}:#{@port}/"])
629     assert_equal String, results[0].class
630     assert_shutdown(pid)
631   end
633   def test_load_module
634     libdir = "#{@tmpdir}/lib"
635     FileUtils.mkpath([ libdir ])
636     config_path = "#{libdir}/hello.rb"
637     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
638     pid = fork do
639       redirect_test_io do
640         Dir.chdir("/")
641         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
642       end
643     end
644     results = retry_hit(["http://#{@addr}:#{@port}/"])
645     assert_equal String, results[0].class
646     assert_shutdown(pid)
647   end
649   def test_reexec
650     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
651     pid_file = "#{@tmpdir}/test.pid"
652     pid = fork do
653       redirect_test_io do
654         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
655       end
656     end
657     reexec_basic_test(pid, pid_file)
658   end
660   def test_reexec_alt_config
661     config_file = "#{@tmpdir}/foo.ru"
662     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
663     pid_file = "#{@tmpdir}/test.pid"
664     pid = fork do
665       redirect_test_io do
666         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
667       end
668     end
669     reexec_basic_test(pid, pid_file)
670   end
672   def test_socket_unlinked_restore
673     results = nil
674     sock = Tempfile.new('unicorn_test_sock')
675     sock_path = sock.path
676     @sockets << sock_path
677     sock.close!
678     ucfg = Tempfile.new('unicorn_test_config')
679     ucfg.syswrite("listen \"#{sock_path}\"\n")
681     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
682     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
683     wait_for_file(sock_path)
684     assert File.socket?(sock_path)
685     assert_nothing_raised do
686       sock = UNIXSocket.new(sock_path)
687       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
688       results = sock.sysread(4096)
689     end
690     assert_equal String, results.class
691     assert_nothing_raised do
692       File.unlink(sock_path)
693       Process.kill(:HUP, pid)
694     end
695     wait_for_file(sock_path)
696     assert File.socket?(sock_path)
697     assert_nothing_raised do
698       sock = UNIXSocket.new(sock_path)
699       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
700       results = sock.sysread(4096)
701     end
702     assert_equal String, results.class
703   end
705   def test_unicorn_config_file
706     pid_file = "#{@tmpdir}/test.pid"
707     sock = Tempfile.new('unicorn_test_sock')
708     sock_path = sock.path
709     sock.close!
710     @sockets << sock_path
712     log = Tempfile.new('unicorn_test_log')
713     ucfg = Tempfile.new('unicorn_test_config')
714     ucfg.syswrite("listen \"#{sock_path}\"\n")
715     ucfg.syswrite("pid \"#{pid_file}\"\n")
716     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
717     ucfg.close
719     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
720     pid = xfork do
721       redirect_test_io do
722         exec($unicorn_bin, "-l#{@addr}:#{@port}",
723              "-P#{pid_file}", "-c#{ucfg.path}")
724       end
725     end
726     results = retry_hit(["http://#{@addr}:#{@port}/"])
727     assert_equal String, results[0].class
728     wait_master_ready(log.path)
729     assert File.exist?(pid_file), "pid_file created"
730     assert_equal pid, File.read(pid_file).to_i
731     assert File.socket?(sock_path), "socket created"
732     assert_nothing_raised do
733       sock = UNIXSocket.new(sock_path)
734       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
735       results = sock.sysread(4096)
736     end
737     assert_equal String, results.class
739     # try reloading the config
740     sock = Tempfile.new('new_test_sock')
741     new_sock_path = sock.path
742     @sockets << new_sock_path
743     sock.close!
744     new_log = Tempfile.new('unicorn_test_log')
745     new_log.sync = true
746     assert_equal 0, new_log.size
748     assert_nothing_raised do
749       ucfg = File.open(ucfg.path, "wb")
750       ucfg.syswrite("listen \"#{sock_path}\"\n")
751       ucfg.syswrite("listen \"#{new_sock_path}\"\n")
752       ucfg.syswrite("pid \"#{pid_file}\"\n")
753       ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
754       ucfg.close
755       Process.kill(:HUP, pid)
756     end
758     wait_for_file(new_sock_path)
759     assert File.socket?(new_sock_path), "socket exists"
760     @sockets.each do |path|
761       assert_nothing_raised do
762         sock = UNIXSocket.new(path)
763         sock.syswrite("GET / HTTP/1.0\r\n\r\n")
764         results = sock.sysread(4096)
765       end
766       assert_equal String, results.class
767     end
769     assert_not_equal 0, new_log.size
770     reexec_usr2_quit_test(pid, pid_file)
771   end
773   def test_daemonize_reexec
774     pid_file = "#{@tmpdir}/test.pid"
775     log = Tempfile.new('unicorn_test_log')
776     ucfg = Tempfile.new('unicorn_test_config')
777     ucfg.syswrite("pid \"#{pid_file}\"\n")
778     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
779     ucfg.close
781     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
782     pid = xfork do
783       redirect_test_io do
784         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
785       end
786     end
787     results = retry_hit(["http://#{@addr}:#{@port}/"])
788     assert_equal String, results[0].class
789     wait_for_file(pid_file)
790     new_pid = File.read(pid_file).to_i
791     assert_not_equal pid, new_pid
792     pid, status = Process.waitpid2(pid)
793     assert status.success?, "original process exited successfully"
794     assert_nothing_raised { Process.kill(0, new_pid) }
795     reexec_usr2_quit_test(new_pid, pid_file)
796   end
798   def test_daemonize_redirect_fail
799     pid_file = "#{@tmpdir}/test.pid"
800     ucfg = Tempfile.new('unicorn_test_config')
801     ucfg.syswrite("pid #{pid_file}\"\n")
802     err = Tempfile.new('stderr')
803     out = Tempfile.new('stdout ')
805     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
806     pid = xfork do
807       $stderr.reopen(err.path, "a")
808       $stdout.reopen(out.path, "a")
809       exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
810     end
811     pid, status = Process.waitpid2(pid)
812     assert ! status.success?, "original process exited successfully"
813     sleep 1 # can't waitpid on a daemonized process :<
814     assert err.stat.size > 0
815   end
817   def test_reexec_fd_leak
818     unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
819       warn "FD leak test only works on Linux at the moment"
820       return
821     end
822     pid_file = "#{@tmpdir}/test.pid"
823     log = Tempfile.new('unicorn_test_log')
824     log.sync = true
825     ucfg = Tempfile.new('unicorn_test_config')
826     ucfg.syswrite("pid \"#{pid_file}\"\n")
827     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
828     ucfg.syswrite("stderr_path '#{log.path}'\n")
829     ucfg.syswrite("stdout_path '#{log.path}'\n")
830     ucfg.close
832     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
833     pid = xfork do
834       redirect_test_io do
835         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
836       end
837     end
839     wait_master_ready(log.path)
840     wait_workers_ready(log.path, 1)
841     File.truncate(log.path, 0)
842     wait_for_file(pid_file)
843     orig_pid = pid = File.read(pid_file).to_i
844     orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
845     assert $?.success?
846     expect_size = orig_fds.size
848     assert_nothing_raised do
849       Process.kill(:USR2, pid)
850       wait_for_file("#{pid_file}.oldbin")
851       Process.kill(:QUIT, pid)
852     end
853     wait_for_death(pid)
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     pid = File.read(pid_file).to_i
860     assert_not_equal orig_pid, pid
861     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
862     assert $?.success?
864     # we could've inherited descriptors the first time around
865     assert expect_size >= curr_fds.size, curr_fds.inspect
866     expect_size = curr_fds.size
868     assert_nothing_raised do
869       Process.kill(:USR2, pid)
870       wait_for_file("#{pid_file}.oldbin")
871       Process.kill(:QUIT, pid)
872     end
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     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
881     assert $?.success?
882     assert_equal expect_size, curr_fds.size, curr_fds.inspect
884     Process.kill(:QUIT, pid)
885     wait_for_death(pid)
886   end
888   def hup_test_common(preload)
889     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
890     pid_file = Tempfile.new('pid')
891     ucfg = Tempfile.new('unicorn_test_config')
892     ucfg.syswrite("listen '#@addr:#@port'\n")
893     ucfg.syswrite("pid '#{pid_file.path}'\n")
894     ucfg.syswrite("preload_app true\n") if preload
895     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
896     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
897     pid = xfork {
898       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
899     }
900     _, status = Process.waitpid2(pid)
901     assert status.success?
902     wait_master_ready("test_stderr.#$$.log")
903     wait_workers_ready("test_stderr.#$$.log", 1)
904     uri = URI.parse("http://#@addr:#@port/")
905     pids = Tempfile.new('worker_pids')
906     r, w = IO.pipe
907     hitter = fork {
908       r.close
909       bodies = Hash.new(0)
910       at_exit { pids.syswrite(bodies.inspect) }
911       trap(:TERM) { exit(0) }
912       nr = 0
913       loop {
914         rv = Net::HTTP.get(uri)
915         pid = rv.to_i
916         exit!(1) if pid <= 0
917         bodies[pid] += 1
918         nr += 1
919         if nr == 1
920           w.syswrite('1')
921         elsif bodies.size > 1
922           w.syswrite('2')
923           sleep
924         end
925       }
926     }
927     w.close
928     assert_equal '1', r.read(1)
929     daemon_pid = File.read(pid_file.path).to_i
930     assert daemon_pid > 0
931     Process.kill(:HUP, daemon_pid)
932     assert_equal '2', r.read(1)
933     assert_nothing_raised { Process.kill(:TERM, hitter) }
934     _, hitter_status = Process.waitpid2(hitter)
935     assert(hitter_status.success?,
936            "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
937            "#{File.read("test_stderr.#$$.log")}")
938     pids.sysseek(0)
939     pids = eval(pids.read)
940     assert_kind_of(Hash, pids)
941     assert_equal 2, pids.size
942     pids.keys.each { |x|
943       assert_kind_of(Integer, x)
944       assert x > 0
945       assert pids[x] > 0
946     }
947     assert_nothing_raised { Process.kill(:QUIT, daemon_pid) }
948     wait_for_death(daemon_pid)
949   end
951   def test_preload_app_hup
952     hup_test_common(true)
953   end
955   def test_hup
956     hup_test_common(false)
957   end
959   def test_default_listen_hup_holds_listener
960     default_listen_lock do
961       res, pid_path = default_listen_setup
962       daemon_pid = File.read(pid_path).to_i
963       assert_nothing_raised { Process.kill(:HUP, daemon_pid) }
964       wait_workers_ready("test_stderr.#$$.log", 1)
965       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
966       assert_match %r{\d+}, res2.first
967       assert res2.first != res.first
968       assert_nothing_raised { Process.kill(:QUIT, daemon_pid) }
969       wait_for_death(daemon_pid)
970     end
971   end
973   def test_default_listen_upgrade_holds_listener
974     default_listen_lock do
975       res, pid_path = default_listen_setup
976       daemon_pid = File.read(pid_path).to_i
977       assert_nothing_raised {
978         Process.kill(:USR2, daemon_pid)
979         wait_for_file("#{pid_path}.oldbin")
980         wait_for_file(pid_path)
981         Process.kill(:QUIT, daemon_pid)
982         wait_for_death(daemon_pid)
983       }
984       daemon_pid = File.read(pid_path).to_i
985       wait_workers_ready("test_stderr.#$$.log", 1)
986       File.truncate("test_stderr.#$$.log", 0)
988       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
989       assert_match %r{\d+}, res2.first
990       assert res2.first != res.first
992       assert_nothing_raised { Process.kill(:HUP, daemon_pid) }
993       wait_workers_ready("test_stderr.#$$.log", 1)
994       File.truncate("test_stderr.#$$.log", 0)
995       res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
996       assert res2.first != res3.first
998       assert_nothing_raised { Process.kill(:QUIT, daemon_pid) }
999       wait_for_death(daemon_pid)
1000     end
1001   end
1003   def default_listen_setup
1004     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
1005     pid_path = (tmp = Tempfile.new('pid')).path
1006     tmp.close!
1007     ucfg = Tempfile.new('unicorn_test_config')
1008     ucfg.syswrite("pid '#{pid_path}'\n")
1009     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1010     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1011     pid = xfork {
1012       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1013     }
1014     _, status = Process.waitpid2(pid)
1015     assert status.success?
1016     wait_master_ready("test_stderr.#$$.log")
1017     wait_workers_ready("test_stderr.#$$.log", 1)
1018     File.truncate("test_stderr.#$$.log", 0)
1019     res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1020     assert_match %r{\d+}, res.first
1021     [ res, pid_path ]
1022   end
1024   # we need to flock() something to prevent these tests from running
1025   def default_listen_lock(&block)
1026     fp = File.open(FLOCK_PATH, "rb")
1027     begin
1028       fp.flock(File::LOCK_EX)
1029       begin
1030         TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1031                       Unicorn::Const::DEFAULT_PORT).close
1032       rescue Errno::EADDRINUSE, Errno::EACCES
1033         warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1034         return false
1035       end
1037       # unused_port should never take this, but we may run an environment
1038       # where tests are being run against older unicorns...
1039       lock_path = "#{Dir::tmpdir}/unicorn_test." \
1040                   "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1041       begin
1042         File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1043         yield
1044       rescue Errno::EEXIST
1045         lock_path = nil
1046         return false
1047       ensure
1048         File.unlink(lock_path) if lock_path
1049       end
1050     ensure
1051       fp.flock(File::LOCK_UN)
1052     end
1053   end
1055 end if do_test