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