FD_CLOEXEC all non-listen descriptors before exec
[unicorn.git] / test / exec / test_exec.rb
blob6c3d28269098cf05d93b7deecc7fe9255c1da4d9
1 # Copyright (c) 2009 Eric Wong
2 require 'test/test_helper'
4 do_test = true
5 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
6 redirect_test_io do
7   do_test = system($unicorn_bin, '-v')
8 end
10 unless do_test
11   warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
12        "skipping this test"
13 end
15 unless try_require('rack')
16   warn "Unable to load Rack, skipping this test"
17   do_test = false
18 end
20 class ExecTest < Test::Unit::TestCase
21   trap(:QUIT, 'IGNORE')
23   HI = <<-EOS
24 use Rack::ContentLength
25 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
26   EOS
28   HELLO = <<-EOS
29 class Hello
30   def call(env)
31     [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
32   end
33 end
34   EOS
36   COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
38   HEAVY_CFG = <<-EOS
39 worker_processes 4
40 timeout 30
41 logger Logger.new('#{COMMON_TMP.path}')
42 before_fork do |server, worker_nr|
43   server.logger.info "before_fork: worker=\#{worker_nr}"
44 end
45   EOS
47   def setup
48     @pwd = Dir.pwd
49     @tmpfile = Tempfile.new('unicorn_exec_test')
50     @tmpdir = @tmpfile.path
51     @tmpfile.close!
52     Dir.mkdir(@tmpdir)
53     Dir.chdir(@tmpdir)
54     @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
55     @port = unused_port(@addr)
56     @sockets = []
57     @start_pid = $$
58   end
60   def teardown
61     return if @start_pid != $$
62     Dir.chdir(@pwd)
63     FileUtils.rmtree(@tmpdir)
64     @sockets.each { |path| File.unlink(path) rescue nil }
65     loop do
66       Process.kill('-QUIT', 0)
67       begin
68         Process.waitpid(-1, Process::WNOHANG) or break
69       rescue Errno::ECHILD
70         break
71       end
72     end
73   end
75   def test_basic
76     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
77     pid = fork do
78       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
79     end
80     results = retry_hit(["http://#{@addr}:#{@port}/"])
81     assert_equal String, results[0].class
82     assert_shutdown(pid)
83   end
85   def test_help
86     redirect_test_io do
87       assert(system($unicorn_bin, "-h"), "help text returns true")
88     end
89     assert_equal 0, File.stat("test_stderr.#$$.log").size
90     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
91     lines = File.readlines("test_stdout.#$$.log")
93     # Be considerate of the on-call technician working from their
94     # mobile phone or netbook on a slow connection :)
95     assert lines.size <= 24, "help height fits in an ANSI terminal window"
96     lines.each do |line|
97       assert line.size <= 80, "help width fits in an ANSI terminal window"
98     end
99   end
101   def test_broken_reexec_config
102     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
103     pid_file = "#{@tmpdir}/test.pid"
104     old_file = "#{pid_file}.oldbin"
105     ucfg = Tempfile.new('unicorn_test_config')
106     ucfg.syswrite("listen %(#@addr:#@port)\n")
107     ucfg.syswrite("pid %(#{pid_file})\n")
108     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
109     pid = xfork do
110       redirect_test_io do
111         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
112       end
113     end
114     results = retry_hit(["http://#{@addr}:#{@port}/"])
115     assert_equal String, results[0].class
117     wait_for_file(pid_file)
118     Process.waitpid(pid)
119     Process.kill(:USR2, File.read(pid_file).to_i)
120     wait_for_file(old_file)
121     wait_for_file(pid_file)
122     old_pid = File.read(old_file).to_i
123     Process.kill(:QUIT, old_pid)
124     wait_for_death(old_pid)
126     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
127     current_pid = File.read(pid_file).to_i
128     Process.kill(:USR2, current_pid)
130     # wait for pid_file to restore itself
131     tries = DEFAULT_TRIES
132     begin
133       while current_pid != File.read(pid_file).to_i
134         sleep(DEFAULT_RES) and (tries -= 1) > 0
135       end
136     rescue Errno::ENOENT
137       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
138     end
139     assert_equal current_pid, File.read(pid_file).to_i
141     tries = DEFAULT_TRIES
142     while File.exist?(old_file)
143       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
144     end
145     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
146     port2 = unused_port(@addr)
148     # fix the bug
149     ucfg.sysseek(0)
150     ucfg.truncate(0)
151     ucfg.syswrite("listen %(#@addr:#@port)\n")
152     ucfg.syswrite("listen %(#@addr:#{port2})\n")
153     ucfg.syswrite("pid %(#{pid_file})\n")
154     assert_nothing_raised { Process.kill(:USR2, current_pid) }
156     wait_for_file(old_file)
157     wait_for_file(pid_file)
158     new_pid = File.read(pid_file).to_i
159     assert_not_equal current_pid, new_pid
160     assert_equal current_pid, File.read(old_file).to_i
161     results = retry_hit(["http://#{@addr}:#{@port}/",
162                          "http://#{@addr}:#{port2}/"])
163     assert_equal String, results[0].class
164     assert_equal String, results[1].class
166     assert_nothing_raised do
167       Process.kill(:QUIT, current_pid)
168       Process.kill(:QUIT, new_pid)
169     end
170   end
172   def test_broken_reexec_ru
173     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
174     pid_file = "#{@tmpdir}/test.pid"
175     old_file = "#{pid_file}.oldbin"
176     ucfg = Tempfile.new('unicorn_test_config')
177     ucfg.syswrite("pid %(#{pid_file})\n")
178     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
179     pid = xfork do
180       redirect_test_io do
181         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
182       end
183     end
184     results = retry_hit(["http://#{@addr}:#{@port}/"])
185     assert_equal String, results[0].class
187     wait_for_file(pid_file)
188     Process.waitpid(pid)
189     Process.kill(:USR2, File.read(pid_file).to_i)
190     wait_for_file(old_file)
191     wait_for_file(pid_file)
192     old_pid = File.read(old_file).to_i
193     Process.kill(:QUIT, old_pid)
194     wait_for_death(old_pid)
196     File.unlink("config.ru") # break reloading
197     current_pid = File.read(pid_file).to_i
198     Process.kill(:USR2, current_pid)
200     # wait for pid_file to restore itself
201     tries = DEFAULT_TRIES
202     begin
203       while current_pid != File.read(pid_file).to_i
204         sleep(DEFAULT_RES) and (tries -= 1) > 0
205       end
206     rescue Errno::ENOENT
207       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
208     end
210     tries = DEFAULT_TRIES
211     while File.exist?(old_file)
212       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
213     end
214     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
215     assert_equal current_pid, File.read(pid_file).to_i
217     # fix the bug
218     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
219     assert_nothing_raised { Process.kill(:USR2, current_pid) }
220     wait_for_file(old_file)
221     wait_for_file(pid_file)
222     new_pid = File.read(pid_file).to_i
223     assert_not_equal current_pid, new_pid
224     assert_equal current_pid, File.read(old_file).to_i
225     results = retry_hit(["http://#{@addr}:#{@port}/"])
226     assert_equal String, results[0].class
228     assert_nothing_raised do
229       Process.kill(:QUIT, current_pid)
230       Process.kill(:QUIT, new_pid)
231     end
232   end
234   def test_unicorn_config_listen_with_options
235     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
236     ucfg = Tempfile.new('unicorn_test_config')
237     ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
238     ucfg.syswrite("                            :rcvbuf => 4096,\n")
239     ucfg.syswrite("                            :sndbuf => 4096\n")
240     pid = xfork do
241       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
242     end
243     results = retry_hit(["http://#{@addr}:#{@port}/"])
244     assert_equal String, results[0].class
245     assert_shutdown(pid)
246   end
248   def test_unicorn_config_per_worker_listen
249     port2 = unused_port
250     pid_spit = 'use Rack::ContentLength;' \
251       'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
252     File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
253     tmp = Tempfile.new('test.socket')
254     File.unlink(tmp.path)
255     ucfg = Tempfile.new('unicorn_test_config')
256     ucfg.syswrite("listen '#@addr:#@port'\n")
257     ucfg.syswrite("before_fork { |s,nr|\n")
258     ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
259     ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
260     ucfg.syswrite("\n}\n")
261     pid = xfork do
262       redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
263     end
264     results = retry_hit(["http://#{@addr}:#{@port}/"])
265     assert_equal String, results[0].class
266     worker_pid = results[0].to_i
267     assert_not_equal pid, worker_pid
268     s = UNIXSocket.new(tmp.path)
269     s.syswrite("GET / HTTP/1.0\r\n\r\n")
270     results = ''
271     loop { results << s.sysread(4096) } rescue nil
272     assert_nothing_raised { s.close }
273     assert_equal worker_pid, results.split(/\r\n/).last.to_i
274     results = hit(["http://#@addr:#{port2}/"])
275     assert_equal String, results[0].class
276     assert_equal worker_pid, results[0].to_i
277     assert_shutdown(pid)
278   end
280   def test_unicorn_config_listen_augments_cli
281     port2 = unused_port(@addr)
282     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
283     ucfg = Tempfile.new('unicorn_test_config')
284     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
285     pid = xfork do
286       redirect_test_io do
287         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
288       end
289     end
290     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
291     results = retry_hit(uris)
292     assert_equal results.size, uris.size
293     assert_equal String, results[0].class
294     assert_equal String, results[1].class
295     assert_shutdown(pid)
296   end
298   def test_weird_config_settings
299     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
300     ucfg = Tempfile.new('unicorn_test_config')
301     ucfg.syswrite(HEAVY_CFG)
302     pid = xfork do
303       redirect_test_io do
304         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
305       end
306     end
308     results = retry_hit(["http://#{@addr}:#{@port}/"])
309     assert_equal String, results[0].class
310     wait_master_ready(COMMON_TMP.path)
311     wait_workers_ready(COMMON_TMP.path, 4)
312     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
313     assert_equal 4, bf.size
314     rotate = Tempfile.new('unicorn_rotate')
315     assert_nothing_raised do
316       File.rename(COMMON_TMP.path, rotate.path)
317       Process.kill(:USR1, pid)
318     end
319     wait_for_file(COMMON_TMP.path)
320     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
321     # USR1 should've been passed to all workers
322     tries = DEFAULT_TRIES
323     log = File.readlines(rotate.path)
324     while (tries -= 1) > 0 &&
325           log.grep(/rotating logs\.\.\./).size < 5
326       sleep DEFAULT_RES
327       log = File.readlines(rotate.path)
328     end
329     assert_equal 5, log.grep(/rotating logs\.\.\./).size
330     assert_equal 0, log.grep(/done rotating logs/).size
332     tries = DEFAULT_TRIES
333     log = File.readlines(COMMON_TMP.path)
334     while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 5
335       sleep DEFAULT_RES
336       log = File.readlines(COMMON_TMP.path)
337     end
338     assert_equal 5, log.grep(/done rotating logs/).size
339     assert_equal 0, log.grep(/rotating logs\.\.\./).size
340     assert_nothing_raised { Process.kill(:QUIT, pid) }
341     status = nil
342     assert_nothing_raised { pid, status = Process.waitpid2(pid) }
343     assert status.success?, "exited successfully"
344   end
346   def test_read_embedded_cli_switches
347     File.open("config.ru", "wb") do |fp|
348       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
349       fp.syswrite(HI)
350     end
351     pid = fork { redirect_test_io { exec($unicorn_bin) } }
352     results = retry_hit(["http://#{@addr}:#{@port}/"])
353     assert_equal String, results[0].class
354     assert_shutdown(pid)
355   end
357   def test_config_ru_alt_path
358     config_path = "#{@tmpdir}/foo.ru"
359     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
360     pid = fork do
361       redirect_test_io do
362         Dir.chdir("/")
363         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
364       end
365     end
366     results = retry_hit(["http://#{@addr}:#{@port}/"])
367     assert_equal String, results[0].class
368     assert_shutdown(pid)
369   end
371   def test_load_module
372     libdir = "#{@tmpdir}/lib"
373     FileUtils.mkpath([ libdir ])
374     config_path = "#{libdir}/hello.rb"
375     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
376     pid = fork do
377       redirect_test_io do
378         Dir.chdir("/")
379         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
380       end
381     end
382     results = retry_hit(["http://#{@addr}:#{@port}/"])
383     assert_equal String, results[0].class
384     assert_shutdown(pid)
385   end
387   def test_reexec
388     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
389     pid_file = "#{@tmpdir}/test.pid"
390     pid = fork do
391       redirect_test_io do
392         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
393       end
394     end
395     reexec_basic_test(pid, pid_file)
396   end
398   def test_reexec_alt_config
399     config_file = "#{@tmpdir}/foo.ru"
400     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
401     pid_file = "#{@tmpdir}/test.pid"
402     pid = fork do
403       redirect_test_io do
404         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
405       end
406     end
407     reexec_basic_test(pid, pid_file)
408   end
410   def test_unicorn_config_file
411     pid_file = "#{@tmpdir}/test.pid"
412     sock = Tempfile.new('unicorn_test_sock')
413     sock_path = sock.path
414     sock.close!
415     @sockets << sock_path
417     log = Tempfile.new('unicorn_test_log')
418     ucfg = Tempfile.new('unicorn_test_config')
419     ucfg.syswrite("listen \"#{sock_path}\"\n")
420     ucfg.syswrite("pid \"#{pid_file}\"\n")
421     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
422     ucfg.close
424     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
425     pid = xfork do
426       redirect_test_io do
427         exec($unicorn_bin, "-l#{@addr}:#{@port}",
428              "-P#{pid_file}", "-c#{ucfg.path}")
429       end
430     end
431     results = retry_hit(["http://#{@addr}:#{@port}/"])
432     assert_equal String, results[0].class
433     wait_master_ready(log.path)
434     assert File.exist?(pid_file), "pid_file created"
435     assert_equal pid, File.read(pid_file).to_i
436     assert File.socket?(sock_path), "socket created"
437     assert_nothing_raised do
438       sock = UNIXSocket.new(sock_path)
439       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
440       results = sock.sysread(4096)
441     end
442     assert_equal String, results.class
444     # try reloading the config
445     sock = Tempfile.new('unicorn_test_sock')
446     new_sock_path = sock.path
447     @sockets << new_sock_path
448     sock.close!
449     new_log = Tempfile.new('unicorn_test_log')
450     new_log.sync = true
451     assert_equal 0, new_log.size
453     assert_nothing_raised do
454       ucfg = File.open(ucfg.path, "wb")
455       ucfg.syswrite("listen \"#{new_sock_path}\"\n")
456       ucfg.syswrite("pid \"#{pid_file}\"\n")
457       ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
458       ucfg.close
459       Process.kill(:HUP, pid)
460     end
462     wait_for_file(new_sock_path)
463     assert File.socket?(new_sock_path), "socket exists"
464     @sockets.each do |path|
465       assert_nothing_raised do
466         sock = UNIXSocket.new(path)
467         sock.syswrite("GET / HTTP/1.0\r\n\r\n")
468         results = sock.sysread(4096)
469       end
470       assert_equal String, results.class
471     end
473     assert_not_equal 0, new_log.size
474     reexec_usr2_quit_test(pid, pid_file)
475   end
477   def test_daemonize_reexec
478     pid_file = "#{@tmpdir}/test.pid"
479     log = Tempfile.new('unicorn_test_log')
480     ucfg = Tempfile.new('unicorn_test_config')
481     ucfg.syswrite("pid \"#{pid_file}\"\n")
482     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
483     ucfg.close
485     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
486     pid = xfork do
487       redirect_test_io do
488         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
489       end
490     end
491     results = retry_hit(["http://#{@addr}:#{@port}/"])
492     assert_equal String, results[0].class
493     wait_for_file(pid_file)
494     new_pid = File.read(pid_file).to_i
495     assert_not_equal pid, new_pid
496     pid, status = Process.waitpid2(pid)
497     assert status.success?, "original process exited successfully"
498     assert_nothing_raised { Process.kill(0, new_pid) }
499     reexec_usr2_quit_test(new_pid, pid_file)
500   end
502   def test_reexec_fd_leak
503     unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
504       warn "FD leak test only works on Linux at the moment"
505       return
506     end
507     pid_file = "#{@tmpdir}/test.pid"
508     log = Tempfile.new('unicorn_test_log')
509     log.sync = true
510     ucfg = Tempfile.new('unicorn_test_config')
511     ucfg.syswrite("pid \"#{pid_file}\"\n")
512     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
513     ucfg.syswrite("stderr_path '#{log.path}'\n")
514     ucfg.syswrite("stdout_path '#{log.path}'\n")
515     ucfg.close
517     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
518     pid = xfork do
519       redirect_test_io do
520         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
521       end
522     end
524     wait_master_ready(log.path)
525     wait_for_file(pid_file)
526     orig_pid = pid = File.read(pid_file).to_i
527     orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
528     assert $?.success?
529     expect_size = orig_fds.size
531     assert_nothing_raised do
532       Process.kill(:USR2, pid)
533       wait_for_file("#{pid_file}.oldbin")
534       Process.kill(:QUIT, pid)
535     end
536     wait_for_death(pid)
538     wait_for_file(pid_file)
539     pid = File.read(pid_file).to_i
540     assert_not_equal orig_pid, pid
541     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
542     assert $?.success?
544     # we could've inherited descriptors the first time around
545     assert expect_size >= curr_fds.size
546     expect_size = curr_fds.size
548     assert_nothing_raised do
549       Process.kill(:USR2, pid)
550       wait_for_file("#{pid_file}.oldbin")
551       Process.kill(:QUIT, pid)
552     end
553     wait_for_death(pid)
555     wait_for_file(pid_file)
556     pid = File.read(pid_file).to_i
557     curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
558     assert $?.success?
559     assert_equal expect_size, curr_fds.size
561     Process.kill(:QUIT, pid)
562     wait_for_death(pid)
563   end
565 end if do_test