test_exec: do not count '\n' as column width
[unicorn.git] / test / exec / test_exec.rb
blob10a1baed2df9c35bf160c6c44af45ad7688067ad
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)
233       Process.kill(sig, pid)
234       pid, status = Process.waitpid2(pid)
236       reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
237       assert_equal 1, reaped.size
238       assert status.exited?
239     end
240   end
242   def test_basic
243     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
244     pid = fork do
245       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
246     end
247     results = retry_hit(["http://#{@addr}:#{@port}/"])
248     assert_equal String, results[0].class
249     assert_shutdown(pid)
250   end
252   def test_rack_env_unset
253     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
254     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
255     results = retry_hit(["http://#{@addr}:#{@port}/"])
256     assert_equal "development", results.first
257     assert_shutdown(pid)
258   end
260   def test_rack_env_cli_set
261     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
262     pid = fork {
263       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
264     }
265     results = retry_hit(["http://#{@addr}:#{@port}/"])
266     assert_equal "asdf", results.first
267     assert_shutdown(pid)
268   end
270   def test_rack_env_ENV_set
271     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
272     pid = fork {
273       ENV["RACK_ENV"] = "foobar"
274       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
275     }
276     results = retry_hit(["http://#{@addr}:#{@port}/"])
277     assert_equal "foobar", results.first
278     assert_shutdown(pid)
279   end
281   def test_rack_env_cli_override_ENV
282     File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
283     pid = fork {
284       ENV["RACK_ENV"] = "foobar"
285       redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
286     }
287     results = retry_hit(["http://#{@addr}:#{@port}/"])
288     assert_equal "asdf", results.first
289     assert_shutdown(pid)
290   end
292   def test_ttin_ttou
293     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
294     pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
295     log = "test_stderr.#{pid}.log"
296     wait_master_ready(log)
297     [ 2, 3].each { |i|
298       Process.kill(:TTIN, pid)
299       wait_workers_ready(log, i)
300     }
301     File.truncate(log, 0)
302     reaped = nil
303     [ 2, 1, 0].each { |i|
304       Process.kill(:TTOU, pid)
305       DEFAULT_TRIES.times {
306         sleep DEFAULT_RES
307         reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
308         break if reaped.size == 1
309       }
310       assert_equal 1, reaped.size
311     }
312   end
314   def test_help
315     redirect_test_io do
316       assert(system($unicorn_bin, "-h"), "help text returns true")
317     end
318     assert_equal 0, File.stat("test_stderr.#$$.log").size
319     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
320     lines = File.readlines("test_stdout.#$$.log")
322     # Be considerate of the on-call technician working from their
323     # mobile phone or netbook on a slow connection :)
324     assert lines.size <= 24, "help height fits in an ANSI terminal window"
325     lines.each do |line|
326       line.chomp!
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     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     Process.kill(:QUIT, current_pid)
397     Process.kill(:QUIT, new_pid)
398   end
400   def test_broken_reexec_ru
401     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
402     pid_file = "#{@tmpdir}/test.pid"
403     old_file = "#{pid_file}.oldbin"
404     ucfg = Tempfile.new('unicorn_test_config')
405     ucfg.syswrite("pid %(#{pid_file})\n")
406     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
407     pid = xfork do
408       redirect_test_io do
409         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
410       end
411     end
412     results = retry_hit(["http://#{@addr}:#{@port}/"])
413     assert_equal String, results[0].class
415     wait_for_file(pid_file)
416     Process.waitpid(pid)
417     Process.kill(:USR2, File.read(pid_file).to_i)
418     wait_for_file(old_file)
419     wait_for_file(pid_file)
420     old_pid = File.read(old_file).to_i
421     Process.kill(:QUIT, old_pid)
422     wait_for_death(old_pid)
424     File.unlink("config.ru") # break reloading
425     current_pid = File.read(pid_file).to_i
426     Process.kill(:USR2, current_pid)
428     # wait for pid_file to restore itself
429     tries = DEFAULT_TRIES
430     begin
431       while current_pid != File.read(pid_file).to_i
432         sleep(DEFAULT_RES) and (tries -= 1) > 0
433       end
434     rescue Errno::ENOENT
435       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
436     end
438     tries = DEFAULT_TRIES
439     while File.exist?(old_file)
440       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
441     end
442     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
443     assert_equal current_pid, File.read(pid_file).to_i
445     # fix the bug
446     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
447     Process.kill(:USR2, current_pid)
448     wait_for_file(old_file)
449     wait_for_file(pid_file)
450     new_pid = File.read(pid_file).to_i
451     assert_not_equal current_pid, new_pid
452     assert_equal current_pid, File.read(old_file).to_i
453     results = retry_hit(["http://#{@addr}:#{@port}/"])
454     assert_equal String, results[0].class
456     Process.kill(:QUIT, current_pid)
457     Process.kill(:QUIT, new_pid)
458   end
460   def test_unicorn_config_listener_swap
461     port_cli = unused_port
462     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
463     ucfg = Tempfile.new('unicorn_test_config')
464     ucfg.syswrite("listen '#@addr:#@port'\n")
465     pid = xfork do
466       redirect_test_io do
467         exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
468       end
469     end
470     results = retry_hit(["http://#@addr:#{port_cli}/"])
471     assert_equal String, results[0].class
472     results = retry_hit(["http://#@addr:#@port/"])
473     assert_equal String, results[0].class
475     port2 = unused_port(@addr)
476     ucfg.sysseek(0)
477     ucfg.truncate(0)
478     ucfg.syswrite("listen '#@addr:#{port2}'\n")
479     Process.kill(:HUP, pid)
481     results = retry_hit(["http://#@addr:#{port2}/"])
482     assert_equal String, results[0].class
483     results = retry_hit(["http://#@addr:#{port_cli}/"])
484     assert_equal String, results[0].class
485     reuse = TCPServer.new(@addr, @port)
486     reuse.close
487     assert_shutdown(pid)
488   end
490   def test_unicorn_config_listen_with_options
491     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
492     ucfg = Tempfile.new('unicorn_test_config')
493     ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
494     ucfg.syswrite("                            :rcvbuf => 4096,\n")
495     ucfg.syswrite("                            :sndbuf => 4096\n")
496     pid = xfork do
497       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
498     end
499     results = retry_hit(["http://#{@addr}:#{@port}/"])
500     assert_equal String, results[0].class
501     assert_shutdown(pid)
502   end
504   def test_unicorn_config_per_worker_listen
505     port2 = unused_port
506     pid_spit = 'use Rack::ContentLength;' \
507       'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
508     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
509     tmp = Tempfile.new('test.socket')
510     File.unlink(tmp.path)
511     ucfg = Tempfile.new('unicorn_test_config')
512     ucfg.syswrite("listen '#@addr:#@port'\n")
513     ucfg.syswrite("after_fork { |s,w|\n")
514     ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
515     ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
516     ucfg.syswrite("\n}\n")
517     pid = xfork do
518       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
519     end
520     results = retry_hit(["http://#{@addr}:#{@port}/"])
521     assert_equal String, results[0].class
522     worker_pid = results[0].to_i
523     assert_not_equal pid, worker_pid
524     s = UNIXSocket.new(tmp.path)
525     s.syswrite("GET / HTTP/1.0\r\n\r\n")
526     results = ''
527     loop { results << s.sysread(4096) } rescue nil
528     s.close
529     assert_equal worker_pid, results.split(/\r\n/).last.to_i
530     results = hit(["http://#@addr:#{port2}/"])
531     assert_equal String, results[0].class
532     assert_equal worker_pid, results[0].to_i
533     assert_shutdown(pid)
534   end
536   def test_unicorn_config_listen_augments_cli
537     port2 = unused_port(@addr)
538     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
539     ucfg = Tempfile.new('unicorn_test_config')
540     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
541     pid = xfork do
542       redirect_test_io do
543         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
544       end
545     end
546     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
547     results = retry_hit(uris)
548     assert_equal results.size, uris.size
549     assert_equal String, results[0].class
550     assert_equal String, results[1].class
551     assert_shutdown(pid)
552   end
554   def test_weird_config_settings
555     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
556     ucfg = Tempfile.new('unicorn_test_config')
557     ucfg.syswrite(HEAVY_CFG)
558     pid = xfork do
559       redirect_test_io do
560         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
561       end
562     end
564     results = retry_hit(["http://#{@addr}:#{@port}/"])
565     assert_equal String, results[0].class
566     wait_master_ready(COMMON_TMP.path)
567     wait_workers_ready(COMMON_TMP.path, 4)
568     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
569     assert_equal 4, bf.size
570     rotate = Tempfile.new('unicorn_rotate')
572     File.rename(COMMON_TMP.path, rotate.path)
573     Process.kill(:USR1, pid)
575     wait_for_file(COMMON_TMP.path)
576     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
577     # USR1 should've been passed to all workers
578     tries = DEFAULT_TRIES
579     log = File.readlines(rotate.path)
580     while (tries -= 1) > 0 &&
581           log.grep(/reopening logs\.\.\./).size < 5
582       sleep DEFAULT_RES
583       log = File.readlines(rotate.path)
584     end
585     assert_equal 5, log.grep(/reopening logs\.\.\./).size
586     assert_equal 0, log.grep(/done reopening logs/).size
588     tries = DEFAULT_TRIES
589     log = File.readlines(COMMON_TMP.path)
590     while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
591       sleep DEFAULT_RES
592       log = File.readlines(COMMON_TMP.path)
593     end
594     assert_equal 5, log.grep(/done reopening logs/).size
595     assert_equal 0, log.grep(/reopening logs\.\.\./).size
597     Process.kill(:QUIT, pid)
598     pid, status = Process.waitpid2(pid)
600     assert status.success?, "exited successfully"
601   end
603   def test_read_embedded_cli_switches
604     File.open("config.ru", "wb") do |fp|
605       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
606       fp.syswrite(HI)
607     end
608     pid = fork { redirect_test_io { exec($unicorn_bin) } }
609     results = retry_hit(["http://#{@addr}:#{@port}/"])
610     assert_equal String, results[0].class
611     assert_shutdown(pid)
612   end
614   def test_config_ru_alt_path
615     config_path = "#{@tmpdir}/foo.ru"
616     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
617     pid = fork do
618       redirect_test_io do
619         Dir.chdir("/")
620         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
621       end
622     end
623     results = retry_hit(["http://#{@addr}:#{@port}/"])
624     assert_equal String, results[0].class
625     assert_shutdown(pid)
626   end
628   def test_load_module
629     libdir = "#{@tmpdir}/lib"
630     FileUtils.mkpath([ libdir ])
631     config_path = "#{libdir}/hello.rb"
632     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
633     pid = fork do
634       redirect_test_io do
635         Dir.chdir("/")
636         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
637       end
638     end
639     results = retry_hit(["http://#{@addr}:#{@port}/"])
640     assert_equal String, results[0].class
641     assert_shutdown(pid)
642   end
644   def test_reexec
645     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
646     pid_file = "#{@tmpdir}/test.pid"
647     pid = fork do
648       redirect_test_io do
649         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
650       end
651     end
652     reexec_basic_test(pid, pid_file)
653   end
655   def test_reexec_alt_config
656     config_file = "#{@tmpdir}/foo.ru"
657     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
658     pid_file = "#{@tmpdir}/test.pid"
659     pid = fork do
660       redirect_test_io do
661         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
662       end
663     end
664     reexec_basic_test(pid, pid_file)
665   end
667   def test_socket_unlinked_restore
668     results = nil
669     sock = Tempfile.new('unicorn_test_sock')
670     sock_path = sock.path
671     @sockets << sock_path
672     sock.close!
673     ucfg = Tempfile.new('unicorn_test_config')
674     ucfg.syswrite("listen \"#{sock_path}\"\n")
676     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
677     pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
678     wait_for_file(sock_path)
679     assert File.socket?(sock_path)
681     sock = UNIXSocket.new(sock_path)
682     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
683     results = sock.sysread(4096)
685     assert_equal String, results.class
686     File.unlink(sock_path)
687     Process.kill(:HUP, pid)
688     wait_for_file(sock_path)
689     assert File.socket?(sock_path)
691     sock = UNIXSocket.new(sock_path)
692     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
693     results = sock.sysread(4096)
695     assert_equal String, results.class
696   end
698   def test_unicorn_config_file
699     pid_file = "#{@tmpdir}/test.pid"
700     sock = Tempfile.new('unicorn_test_sock')
701     sock_path = sock.path
702     sock.close!
703     @sockets << sock_path
705     log = Tempfile.new('unicorn_test_log')
706     ucfg = Tempfile.new('unicorn_test_config')
707     ucfg.syswrite("listen \"#{sock_path}\"\n")
708     ucfg.syswrite("pid \"#{pid_file}\"\n")
709     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
710     ucfg.close
712     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
713     pid = xfork do
714       redirect_test_io do
715         exec($unicorn_bin, "-l#{@addr}:#{@port}",
716              "-P#{pid_file}", "-c#{ucfg.path}")
717       end
718     end
719     results = retry_hit(["http://#{@addr}:#{@port}/"])
720     assert_equal String, results[0].class
721     wait_master_ready(log.path)
722     assert File.exist?(pid_file), "pid_file created"
723     assert_equal pid, File.read(pid_file).to_i
724     assert File.socket?(sock_path), "socket created"
726     sock = UNIXSocket.new(sock_path)
727     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
728     results = sock.sysread(4096)
730     assert_equal String, results.class
732     # try reloading the config
733     sock = Tempfile.new('new_test_sock')
734     new_sock_path = sock.path
735     @sockets << new_sock_path
736     sock.close!
737     new_log = Tempfile.new('unicorn_test_log')
738     new_log.sync = true
739     assert_equal 0, new_log.size
741     ucfg = File.open(ucfg.path, "wb")
742     ucfg.syswrite("listen \"#{sock_path}\"\n")
743     ucfg.syswrite("listen \"#{new_sock_path}\"\n")
744     ucfg.syswrite("pid \"#{pid_file}\"\n")
745     ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
746     ucfg.close
747     Process.kill(:HUP, pid)
749     wait_for_file(new_sock_path)
750     assert File.socket?(new_sock_path), "socket exists"
751     @sockets.each do |path|
752       sock = UNIXSocket.new(path)
753       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
754       results = sock.sysread(4096)
755       assert_equal String, results.class
756     end
758     assert_not_equal 0, new_log.size
759     reexec_usr2_quit_test(pid, pid_file)
760   end
762   def test_daemonize_reexec
763     pid_file = "#{@tmpdir}/test.pid"
764     log = Tempfile.new('unicorn_test_log')
765     ucfg = Tempfile.new('unicorn_test_config')
766     ucfg.syswrite("pid \"#{pid_file}\"\n")
767     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
768     ucfg.close
770     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
771     pid = xfork do
772       redirect_test_io do
773         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
774       end
775     end
776     results = retry_hit(["http://#{@addr}:#{@port}/"])
777     assert_equal String, results[0].class
778     wait_for_file(pid_file)
779     new_pid = File.read(pid_file).to_i
780     assert_not_equal pid, new_pid
781     pid, status = Process.waitpid2(pid)
782     assert status.success?, "original process exited successfully"
783     Process.kill(0, new_pid)
784     reexec_usr2_quit_test(new_pid, pid_file)
785   end
787   def test_daemonize_redirect_fail
788     pid_file = "#{@tmpdir}/test.pid"
789     ucfg = Tempfile.new('unicorn_test_config')
790     ucfg.syswrite("pid #{pid_file}\"\n")
791     err = Tempfile.new('stderr')
792     out = Tempfile.new('stdout ')
794     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
795     pid = xfork do
796       $stderr.reopen(err.path, "a")
797       $stdout.reopen(out.path, "a")
798       exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
799     end
800     pid, status = Process.waitpid2(pid)
801     assert ! status.success?, "original process exited successfully"
802     sleep 1 # can't waitpid on a daemonized process :<
803     assert err.stat.size > 0
804   end
806   def test_reexec_fd_leak
807     unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
808       warn "FD leak test only works on Linux at the moment"
809       return
810     end
811     pid_file = "#{@tmpdir}/test.pid"
812     log = Tempfile.new('unicorn_test_log')
813     log.sync = true
814     ucfg = Tempfile.new('unicorn_test_config')
815     ucfg.syswrite("pid \"#{pid_file}\"\n")
816     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
817     ucfg.syswrite("stderr_path '#{log.path}'\n")
818     ucfg.syswrite("stdout_path '#{log.path}'\n")
819     ucfg.close
821     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
822     pid = xfork do
823       redirect_test_io do
824         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
825       end
826     end
828     wait_master_ready(log.path)
829     wait_workers_ready(log.path, 1)
830     File.truncate(log.path, 0)
831     wait_for_file(pid_file)
832     orig_pid = pid = File.read(pid_file).to_i
833     orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
834     assert $?.success?
835     expect_size = orig_fds.size
837     Process.kill(:USR2, pid)
838     wait_for_file("#{pid_file}.oldbin")
839     Process.kill(:QUIT, pid)
841     wait_for_death(pid)
843     wait_master_ready(log.path)
844     wait_workers_ready(log.path, 1)
845     File.truncate(log.path, 0)
846     wait_for_file(pid_file)
847     pid = File.read(pid_file).to_i
848     assert_not_equal orig_pid, pid
849     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
850     assert $?.success?
852     # we could've inherited descriptors the first time around
853     assert expect_size >= curr_fds.size, curr_fds.inspect
854     expect_size = curr_fds.size
856     Process.kill(:USR2, pid)
857     wait_for_file("#{pid_file}.oldbin")
858     Process.kill(:QUIT, pid)
860     wait_for_death(pid)
862     wait_master_ready(log.path)
863     wait_workers_ready(log.path, 1)
864     File.truncate(log.path, 0)
865     wait_for_file(pid_file)
866     pid = File.read(pid_file).to_i
867     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
868     assert $?.success?
869     assert_equal expect_size, curr_fds.size, curr_fds.inspect
871     Process.kill(:QUIT, pid)
872     wait_for_death(pid)
873   end
875   def hup_test_common(preload, check_client=false)
876     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
877     pid_file = Tempfile.new('pid')
878     ucfg = Tempfile.new('unicorn_test_config')
879     ucfg.syswrite("listen '#@addr:#@port'\n")
880     ucfg.syswrite("pid '#{pid_file.path}'\n")
881     ucfg.syswrite("preload_app true\n") if preload
882     ucfg.syswrite("check_client_connection true\n") if check_client
883     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
884     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
885     pid = xfork {
886       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
887     }
888     _, status = Process.waitpid2(pid)
889     assert status.success?
890     wait_master_ready("test_stderr.#$$.log")
891     wait_workers_ready("test_stderr.#$$.log", 1)
892     uri = URI.parse("http://#@addr:#@port/")
893     pids = Tempfile.new('worker_pids')
894     r, w = IO.pipe
895     hitter = fork {
896       r.close
897       bodies = Hash.new(0)
898       at_exit { pids.syswrite(bodies.inspect) }
899       trap(:TERM) { exit(0) }
900       nr = 0
901       loop {
902         rv = Net::HTTP.get(uri)
903         pid = rv.to_i
904         exit!(1) if pid <= 0
905         bodies[pid] += 1
906         nr += 1
907         if nr == 1
908           w.syswrite('1')
909         elsif bodies.size > 1
910           w.syswrite('2')
911           sleep
912         end
913       }
914     }
915     w.close
916     assert_equal '1', r.read(1)
917     daemon_pid = File.read(pid_file.path).to_i
918     assert daemon_pid > 0
919     Process.kill(:HUP, daemon_pid)
920     assert_equal '2', r.read(1)
921     Process.kill(:TERM, hitter)
922     _, hitter_status = Process.waitpid2(hitter)
923     assert(hitter_status.success?,
924            "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
925            "#{File.read("test_stderr.#$$.log")}")
926     pids.sysseek(0)
927     pids = eval(pids.read)
928     assert_kind_of(Hash, pids)
929     assert_equal 2, pids.size
930     pids.keys.each { |x|
931       assert_kind_of(Integer, x)
932       assert x > 0
933       assert pids[x] > 0
934     }
935     Process.kill(:QUIT, daemon_pid)
936     wait_for_death(daemon_pid)
937   end
939   def test_preload_app_hup
940     hup_test_common(true)
941   end
943   def test_hup
944     hup_test_common(false)
945   end
947   def test_check_client_hup
948     hup_test_common(false, true)
949   end
951   def test_default_listen_hup_holds_listener
952     default_listen_lock do
953       res, pid_path = default_listen_setup
954       daemon_pid = File.read(pid_path).to_i
955       Process.kill(:HUP, daemon_pid)
956       wait_workers_ready("test_stderr.#$$.log", 1)
957       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
958       assert_match %r{\d+}, res2.first
959       assert res2.first != res.first
960       Process.kill(:QUIT, daemon_pid)
961       wait_for_death(daemon_pid)
962     end
963   end
965   def test_default_listen_upgrade_holds_listener
966     default_listen_lock do
967       res, pid_path = default_listen_setup
968       daemon_pid = File.read(pid_path).to_i
970       Process.kill(:USR2, daemon_pid)
971       wait_for_file("#{pid_path}.oldbin")
972       wait_for_file(pid_path)
973       Process.kill(:QUIT, daemon_pid)
974       wait_for_death(daemon_pid)
976       daemon_pid = File.read(pid_path).to_i
977       wait_workers_ready("test_stderr.#$$.log", 1)
978       File.truncate("test_stderr.#$$.log", 0)
980       res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
981       assert_match %r{\d+}, res2.first
982       assert res2.first != res.first
984       Process.kill(:HUP, daemon_pid)
985       wait_workers_ready("test_stderr.#$$.log", 1)
986       File.truncate("test_stderr.#$$.log", 0)
987       res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
988       assert res2.first != res3.first
990       Process.kill(:QUIT, daemon_pid)
991       wait_for_death(daemon_pid)
992     end
993   end
995   def default_listen_setup
996     File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
997     pid_path = (tmp = Tempfile.new('pid')).path
998     tmp.close!
999     ucfg = Tempfile.new('unicorn_test_config')
1000     ucfg.syswrite("pid '#{pid_path}'\n")
1001     ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1002     ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1003     pid = xfork {
1004       redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1005     }
1006     _, status = Process.waitpid2(pid)
1007     assert status.success?
1008     wait_master_ready("test_stderr.#$$.log")
1009     wait_workers_ready("test_stderr.#$$.log", 1)
1010     File.truncate("test_stderr.#$$.log", 0)
1011     res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1012     assert_match %r{\d+}, res.first
1013     [ res, pid_path ]
1014   end
1016   # we need to flock() something to prevent these tests from running
1017   def default_listen_lock(&block)
1018     fp = File.open(FLOCK_PATH, "rb")
1019     begin
1020       fp.flock(File::LOCK_EX)
1021       begin
1022         TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1023                       Unicorn::Const::DEFAULT_PORT).close
1024       rescue Errno::EADDRINUSE, Errno::EACCES
1025         warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1026         return false
1027       end
1029       # unused_port should never take this, but we may run an environment
1030       # where tests are being run against older unicorns...
1031       lock_path = "#{Dir::tmpdir}/unicorn_test." \
1032                   "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1033       begin
1034         File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1035         yield
1036       rescue Errno::EEXIST
1037         lock_path = nil
1038         return false
1039       ensure
1040         File.unlink(lock_path) if lock_path
1041       end
1042     ensure
1043       fp.flock(File::LOCK_UN)
1044     end
1045   end
1047 end if do_test