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