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