yahns 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Documentation/.gitignore +5 -0
  4. data/Documentation/GNUmakefile +50 -0
  5. data/Documentation/yahns-rackup.txt +152 -0
  6. data/Documentation/yahns.txt +68 -0
  7. data/Documentation/yahns_config.txt +563 -0
  8. data/GIT-VERSION-GEN +1 -1
  9. data/GNUmakefile +14 -7
  10. data/HACKING +56 -0
  11. data/INSTALL +8 -0
  12. data/README +15 -2
  13. data/Rakefile +2 -2
  14. data/bin/yahns +1 -2
  15. data/bin/yahns-rackup +9 -0
  16. data/examples/yahns_multi.conf.rb +14 -4
  17. data/examples/yahns_rack_basic.conf.rb +17 -1
  18. data/extras/README +16 -0
  19. data/extras/autoindex.rb +151 -0
  20. data/extras/exec_cgi.rb +108 -0
  21. data/extras/proxy_pass.rb +210 -0
  22. data/extras/try_gzip_static.rb +208 -0
  23. data/lib/yahns.rb +5 -2
  24. data/lib/yahns/acceptor.rb +64 -22
  25. data/lib/yahns/cap_input.rb +2 -2
  26. data/lib/yahns/{client_expire_portable.rb → client_expire_generic.rb} +12 -11
  27. data/lib/yahns/{client_expire.rb → client_expire_tcpi.rb} +7 -6
  28. data/lib/yahns/config.rb +107 -22
  29. data/lib/yahns/daemon.rb +2 -0
  30. data/lib/yahns/fdmap.rb +28 -9
  31. data/lib/yahns/http_client.rb +123 -37
  32. data/lib/yahns/http_context.rb +21 -3
  33. data/lib/yahns/http_response.rb +80 -19
  34. data/lib/yahns/log.rb +23 -4
  35. data/lib/yahns/queue_epoll.rb +20 -9
  36. data/lib/yahns/queue_quitter.rb +16 -0
  37. data/lib/yahns/queue_quitter_pipe.rb +24 -0
  38. data/lib/yahns/rack.rb +0 -1
  39. data/lib/yahns/rackup_handler.rb +57 -0
  40. data/lib/yahns/server.rb +189 -59
  41. data/lib/yahns/server_mp.rb +43 -35
  42. data/lib/yahns/sigevent_pipe.rb +1 -0
  43. data/lib/yahns/socket_helper.rb +37 -11
  44. data/lib/yahns/stream_file.rb +14 -4
  45. data/lib/yahns/stream_input.rb +13 -7
  46. data/lib/yahns/tcp_server.rb +7 -0
  47. data/lib/yahns/tmpio.rb +10 -3
  48. data/lib/yahns/unix_server.rb +7 -0
  49. data/lib/yahns/wbuf.rb +19 -2
  50. data/lib/yahns/wbuf_common.rb +10 -3
  51. data/lib/yahns/wbuf_str.rb +24 -0
  52. data/lib/yahns/worker.rb +5 -26
  53. data/test/helper.rb +15 -5
  54. data/test/server_helper.rb +37 -1
  55. data/test/test_bin.rb +17 -8
  56. data/test/test_buffer_tmpdir.rb +103 -0
  57. data/test/test_client_expire.rb +71 -35
  58. data/test/test_client_max_body_size.rb +5 -13
  59. data/test/test_config.rb +1 -1
  60. data/test/test_expect_100.rb +176 -0
  61. data/test/test_extras_autoindex.rb +53 -0
  62. data/test/test_extras_exec_cgi.rb +81 -0
  63. data/test/test_extras_exec_cgi.sh +35 -0
  64. data/test/test_extras_try_gzip_static.rb +177 -0
  65. data/test/test_input.rb +128 -0
  66. data/test/test_mt_accept.rb +48 -0
  67. data/test/test_output_buffering.rb +90 -63
  68. data/test/test_rack.rb +1 -1
  69. data/test/test_rack_hijack.rb +2 -6
  70. data/test/test_reopen_logs.rb +2 -8
  71. data/test/test_serve_static.rb +104 -8
  72. data/test/test_server.rb +448 -73
  73. data/test/test_stream_file.rb +1 -1
  74. data/test/test_unix_socket.rb +72 -0
  75. data/test/test_wbuf.rb +20 -17
  76. data/yahns.gemspec +3 -0
  77. metadata +57 -5
@@ -4,7 +4,7 @@ require_relative 'server_helper'
4
4
  require 'rack/commonlogger'
5
5
 
6
6
  class TestReopenLogs < Testcase
7
- parallelize_me!
7
+ ENV["N"].to_i > 1 and parallelize_me!
8
8
  include ServerHelper
9
9
  alias setup server_helper_setup
10
10
  alias teardown server_helper_teardown
@@ -32,10 +32,7 @@ class TestReopenLogs < Testcase
32
32
  end
33
33
  worker_processes 1 if worker
34
34
  end
35
- pid = fork do
36
- ENV["YAHNS_FD"] = @srv.fileno.to_s
37
- Yahns::Server.new(cfg).start.join
38
- end
35
+ pid = mkserver(cfg)
39
36
  Net::HTTP.start(host, port) do |http|
40
37
  res = http.request(Net::HTTP::Get.new("/aaa"))
41
38
  assert_equal 200, res.code.to_i
@@ -55,9 +52,6 @@ class TestReopenLogs < Testcase
55
52
  end until File.read(opath) =~ /bbb/
56
53
  end
57
54
  end
58
- rescue => e
59
- Yahns::Log.exception(Logger.new($stderr), "test", e)
60
- raise
61
55
  ensure
62
56
  quit_wait(pid)
63
57
  end
@@ -4,12 +4,50 @@ require_relative 'server_helper'
4
4
  require 'rack/file'
5
5
 
6
6
  class TestServeStatic < Testcase
7
- parallelize_me!
7
+ ENV["N"].to_i > 1 and parallelize_me!
8
8
  include ServerHelper
9
9
  alias setup server_helper_setup
10
10
  alias teardown server_helper_teardown
11
11
 
12
12
  def test_serve_static
13
+ tmpdir = Dir.mktmpdir
14
+ sock = "#{tmpdir}/sock"
15
+ err = @err
16
+ cfg = Yahns::Config.new
17
+ host, port = @srv.addr[3], @srv.addr[1]
18
+ cfg.instance_eval do
19
+ GTL.synchronize do
20
+ app(:rack, Rack::File.new(Dir.pwd)) {
21
+ listen sock
22
+ listen "#{host}:#{port}"
23
+ }
24
+ end
25
+ logger(Logger.new(err.path))
26
+ end
27
+ pid = mkserver(cfg)
28
+ gplv3 = File.read("COPYING")
29
+ Net::HTTP.start(host, port) do |http|
30
+ res = http.request(Net::HTTP::Get.new("/COPYING"))
31
+ assert_equal gplv3, res.body
32
+
33
+ req = Net::HTTP::Get.new("/COPYING", "Range" => "bytes=5-46")
34
+ res = http.request(req)
35
+ assert_match %r{bytes 5-46/\d+\z}, res["Content-Range"]
36
+ assert_equal gplv3[5..46], res.body
37
+ end
38
+
39
+ # ensure sendfile works on Unix sockets
40
+ s = UNIXSocket.new(sock)
41
+ s.close_on_exec = true
42
+ s.write "GET /COPYING\r\n\r\n"
43
+ assert_equal gplv3, Timeout.timeout(30) { s.read }
44
+ s.close
45
+ ensure
46
+ quit_wait(pid)
47
+ FileUtils.rm_rf tmpdir
48
+ end
49
+
50
+ def test_serve_static_blocked_header
13
51
  err = @err
14
52
  cfg = Yahns::Config.new
15
53
  host, port = @srv.addr[3], @srv.addr[1]
@@ -19,10 +57,10 @@ class TestServeStatic < Testcase
19
57
  end
20
58
  logger(Logger.new(err.path))
21
59
  end
22
- srv = Yahns::Server.new(cfg)
23
- pid = fork do
24
- ENV["YAHNS_FD"] = @srv.fileno.to_s
25
- srv.start.join
60
+ pid = mkserver(cfg) do
61
+ $_tw_blocked = 0
62
+ $_tw_block_on = [1]
63
+ Yahns::HttpClient.__send__(:include, TrywriteBlocked)
26
64
  end
27
65
  gplv3 = File.read("COPYING")
28
66
  Net::HTTP.start(host, port) do |http|
@@ -33,10 +71,68 @@ class TestServeStatic < Testcase
33
71
  res = http.request(req)
34
72
  assert_equal gplv3[5..46], res.body
35
73
  end
36
- rescue => e
37
- Yahns::Log.exception(Logger.new($stderr), "test", e)
38
- raise
39
74
  ensure
40
75
  quit_wait(pid)
41
76
  end
77
+
78
+ def mksparse(tmpdir)
79
+ sparse = "#{tmpdir}/sparse"
80
+ off = 100 * 1024 * 1024
81
+ File.open(sparse, "w") do |fp|
82
+ fp.sysseek(off)
83
+ fp.syswrite '.'
84
+ end
85
+ [ off + 1, sparse ]
86
+ end
87
+
88
+ def test_truncated_sendfile
89
+ tmpdir = Dir.mktmpdir
90
+ size, sparse = mksparse(tmpdir)
91
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
92
+ pid = mkserver(cfg) do
93
+ cfg.instance_eval do
94
+ app(:rack, Rack::File.new(tmpdir)) { listen "#{host}:#{port}" }
95
+ stderr_path err.path
96
+ end
97
+ end
98
+ c = get_tcp_client(host, port)
99
+ c.write "GET /sparse HTTP/1.1\r\nHost: example.com\r\n\r\n"
100
+ wait_for_full(c)
101
+ File.truncate(sparse, 5)
102
+ buf = Timeout.timeout(60) { c.read }
103
+ c.close
104
+ assert_operator buf.size, :<, size
105
+ ensure
106
+ quit_wait(pid)
107
+ FileUtils.rm_rf(tmpdir)
108
+ end
109
+
110
+ def test_expanded_sendfile
111
+ tmpdir = Dir.mktmpdir
112
+ size, sparse = mksparse(tmpdir)
113
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
114
+ pid = mkserver(cfg) do
115
+ cfg.instance_eval do
116
+ app(:rack, Rack::File.new(tmpdir)) { listen "#{host}:#{port}" }
117
+ stderr_path err.path
118
+ end
119
+ end
120
+ c = get_tcp_client(host, port)
121
+ c.write "GET /sparse\r\n\r\n"
122
+ wait_for_full(c)
123
+
124
+ File.open(sparse, "w") do |fp|
125
+ fp.sysseek(size * 2)
126
+ fp.syswrite '.'
127
+ end
128
+ Timeout.timeout(60) do
129
+ bytes = IO.copy_stream(c, "/dev/null")
130
+ assert_equal bytes, size
131
+ assert_raises(EOFError) { c.readpartial 1 }
132
+ end
133
+ c.close
134
+ ensure
135
+ quit_wait(pid)
136
+ FileUtils.rm_rf(tmpdir)
137
+ end
42
138
  end
data/test/test_server.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require_relative 'server_helper'
4
4
 
5
5
  class TestServer < Testcase
6
- parallelize_me!
6
+ ENV["N"].to_i > 1 and parallelize_me!
7
7
  include ServerHelper
8
8
 
9
9
  alias setup server_helper_setup
@@ -18,13 +18,9 @@ class TestServer < Testcase
18
18
  GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
19
19
  logger(Logger.new(err.path))
20
20
  end
21
- srv = Yahns::Server.new(cfg)
22
- pid = fork do
23
- ENV["YAHNS_FD"] = @srv.fileno.to_s
24
- srv.start.join
25
- end
21
+ pid = mkserver(cfg)
26
22
  run_client(host, port) { |res| assert_equal "HI", res.body }
27
- c = TCPSocket.new(host, port)
23
+ c = get_tcp_client(host, port)
28
24
 
29
25
  # test pipelining
30
26
  r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
@@ -60,9 +56,6 @@ class TestServer < Testcase
60
56
  _, status = Timeout.timeout(10) { Process.waitpid2(pid) }
61
57
  assert status.success?, status.inspect
62
58
  c.close
63
- rescue => e
64
- Yahns::Log.exception(Logger.new($stderr), "test", e)
65
- raise
66
59
  end
67
60
 
68
61
  def test_input_body_true; input_body(true); end
@@ -83,12 +76,8 @@ class TestServer < Testcase
83
76
  end
84
77
  logger(Logger.new(err.path))
85
78
  end
86
- srv = Yahns::Server.new(cfg)
87
- pid = fork do
88
- ENV["YAHNS_FD"] = @srv.fileno.to_s
89
- srv.start.join
90
- end
91
- c = TCPSocket.new(host, port)
79
+ pid = mkserver(cfg)
80
+ c = get_tcp_client(host, port)
92
81
  buf = "PUT / HTTP/1.0\r\nContent-Length: 2\r\n\r\nHI"
93
82
  c.write(buf)
94
83
  IO.select([c], nil, nil, 5)
@@ -100,7 +89,7 @@ class TestServer < Testcase
100
89
 
101
90
  # pipelined oneshot
102
91
  buf = "PUT / HTTP/1.1\r\nContent-Length: 2\r\n\r\nHI"
103
- c = TCPSocket.new(host, port)
92
+ c = get_tcp_client(host, port)
104
93
  c.write(buf + buf)
105
94
  buf = ""
106
95
  Timeout.timeout(10) do
@@ -132,9 +121,6 @@ class TestServer < Testcase
132
121
  first = $1
133
122
  assert rv
134
123
  assert_equal first, buf
135
- rescue => e
136
- Yahns::Log.exception(Logger.new($stderr), "test", e)
137
- raise
138
124
  ensure
139
125
  c.close if c
140
126
  quit_wait(pid)
@@ -165,12 +151,8 @@ class TestServer < Testcase
165
151
  end
166
152
  logger(Logger.new(err.path))
167
153
  end
168
- srv = Yahns::Server.new(cfg)
169
- pid = fork do
170
- ENV["YAHNS_FD"] = @srv.fileno.to_s
171
- srv.start.join
172
- end
173
- c = TCPSocket.new(host, port)
154
+ pid = mkserver(cfg)
155
+ c = get_tcp_client(host, port)
174
156
  buf = "PUT / HTTP/1.0\r\nTrailer:xbt\r\nTransfer-Encoding: chunked\r\n\r\n"
175
157
  c.write(buf)
176
158
  xbt = btype.to_s
@@ -199,7 +181,7 @@ class TestServer < Testcase
199
181
  msgs = %w(ZZ zz)
200
182
  err = @err
201
183
  cfg = Yahns::Config.new
202
- bpipe = IO.pipe
184
+ bpipe = cloexec_pipe
203
185
  host, port = @srv.addr[3], @srv.addr[1]
204
186
  cfg.instance_eval do
205
187
  ru = lambda { |e|
@@ -237,7 +219,7 @@ class TestServer < Testcase
237
219
  # ensure we set worker_threads correctly
238
220
  eggs = srv.instance_variable_get(:@config).qeggs
239
221
  assert_equal 1, eggs.size
240
- assert_equal 1, eggs[:default].instance_variable_get(:@worker_threads)
222
+ assert_equal 1, eggs.first[1].instance_variable_get(:@worker_threads)
241
223
 
242
224
  pid = fork do
243
225
  bpipe[1].close
@@ -245,8 +227,8 @@ class TestServer < Testcase
245
227
  srv.start.join
246
228
  end
247
229
  bpipe[0].close
248
- a = TCPSocket.new(host, port)
249
- b = TCPSocket.new(host, port)
230
+ a = get_tcp_client(host, port)
231
+ b = get_tcp_client(host, port)
250
232
  a.write("GET /sleep HTTP/1.0\r\n\r\n")
251
233
  r = IO.select([a], nil, nil, 4)
252
234
  assert r, "nothing ready"
@@ -288,37 +270,6 @@ class TestServer < Testcase
288
270
  end
289
271
  end
290
272
 
291
- # Linux blocking accept() has fair behavior between multiple tasks
292
- def test_mp_balance
293
- skip("this fails occasionally on Linux, still...")
294
- skip("linux-only test") unless RUBY_PLATFORM =~ /linux/
295
- pid, host, port = new_mp_server(2)
296
- seen = {}
297
-
298
- # wait for both processes to spin up
299
- Timeout.timeout(10) do
300
- run_client(host, port) { |res| seen[res.body] = 1 } until seen.size == 2
301
- end
302
-
303
- prev = nil
304
- req = Net::HTTP::Get.new("/")
305
- # we should bounce new connections between 2 processes
306
- 4.times do
307
- Net::HTTP.start(host, port) do |http|
308
- res = http.request(req)
309
- assert_equal 200, res.code.to_i
310
- assert_equal "keep-alive", res["Connection"]
311
- refute_equal prev, res.body, "same PID accepted twice"
312
- prev = res.body.dup
313
- seen[prev] += 1
314
- 666.times { Thread.pass } # have the other acceptor to wake up
315
- end
316
- end
317
- assert_equal 2, seen.size
318
- ensure
319
- quit_wait(pid)
320
- end
321
-
322
273
  def test_mp_worker_die
323
274
  pid, host, port = new_mp_server
324
275
  wpid1 = wpid2 = nil
@@ -349,7 +300,7 @@ class TestServer < Testcase
349
300
  end
350
301
 
351
302
  def run_client(host, port)
352
- c = TCPSocket.new(host, port)
303
+ c = get_tcp_client(host, port)
353
304
  Net::HTTP.start(host, port) do |http|
354
305
  res = http.request(Net::HTTP::Get.new("/"))
355
306
  assert_equal 200, res.code.to_i
@@ -381,11 +332,7 @@ class TestServer < Testcase
381
332
  GTL.synchronize { app(:rack, ru.path) { listen "#{host}:#{port}" } }
382
333
  logger(Logger.new(File.open(err.path, "a")))
383
334
  end
384
- srv = Yahns::Server.new(cfg)
385
- pid = fork do
386
- ENV["YAHNS_FD"] = @srv.fileno.to_s
387
- srv.start.join
388
- end
335
+ pid = mkserver(cfg)
389
336
  [ pid, host, port ]
390
337
  end
391
338
 
@@ -403,12 +350,8 @@ class TestServer < Testcase
403
350
  }
404
351
  logger(Logger.new(err.path))
405
352
  end
406
- srv = Yahns::Server.new(cfg)
407
- pid = fork do
408
- ENV["YAHNS_FD"] = @srv.fileno.to_s
409
- srv.start.join
410
- end
411
- c = TCPSocket.new(host, port)
353
+ pid = mkserver(cfg)
354
+ c = get_tcp_client(host, port)
412
355
  c.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
413
356
  buf = Timeout.timeout(10) { c.read }
414
357
  assert_match(/Connection: close/, buf)
@@ -416,4 +359,436 @@ class TestServer < Testcase
416
359
  ensure
417
360
  quit_wait(pid)
418
361
  end
362
+
363
+ def test_ttin_ttou
364
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
365
+ ru = lambda { |_|
366
+ b = "#$$"
367
+ [ 200, {'Content-Length'=>b.size.to_s}, [b] ]
368
+ }
369
+ cfg.instance_eval do
370
+ GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
371
+ worker_processes 1
372
+ stderr_path err.path
373
+ end
374
+ pid = mkserver(cfg)
375
+
376
+ read_pid = lambda do
377
+ c = get_tcp_client(host, port)
378
+ c.write("GET /\r\n\r\n")
379
+ body = Timeout.timeout(10) { c.read }
380
+ c.close
381
+ assert_match(/\A\d+\z/, body)
382
+ body
383
+ end
384
+
385
+ orig_worker_pid = read_pid.call.to_i
386
+ assert_equal 1, Process.kill(0, orig_worker_pid)
387
+
388
+ Process.kill(:TTOU, pid)
389
+ poke_until_dead(orig_worker_pid)
390
+
391
+ Process.kill(:TTIN, pid)
392
+ second_worker_pid = read_pid.call.to_i
393
+
394
+ # PID recycling is rare, hope it doesn't fail here
395
+ refute_equal orig_worker_pid, second_worker_pid
396
+ ensure
397
+ quit_wait(pid)
398
+ end
399
+
400
+ def test_mp_hooks
401
+ err = @err
402
+ out = tmpfile(%w(mp_hooks .out))
403
+ cfg = Yahns::Config.new
404
+ host, port = @srv.addr[3], @srv.addr[1]
405
+ cfg.instance_eval do
406
+ ru = lambda {|_|x="#$$";[200,{'Content-Length'=>x.size.to_s },[x]]}
407
+ GTL.synchronize {
408
+ app(:rack, ru) {
409
+ listen "#{host}:#{port}"
410
+ persistent_connections false
411
+ atfork_child { warn "INFO hihi from app.atfork_child" }
412
+ }
413
+ worker_processes(1) do
414
+ atfork_child { puts "af #$$ worker is running" }
415
+ atfork_prepare { puts "af #$$ parent about to spawn" }
416
+ atfork_parent { puts "af #$$ this is probably not useful" }
417
+ end
418
+ }
419
+ stderr_path err.path
420
+ stdout_path out.path
421
+ end
422
+ pid = mkserver(cfg)
423
+ c = get_tcp_client(host, port)
424
+ c.write("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
425
+ buf = Timeout.timeout(10) { c.read }
426
+ c.close
427
+ head, body = buf.split(/\r\n\r\n/)
428
+ assert_match(/200 OK/, head)
429
+ assert_match(/\A\d+\z/, body)
430
+ worker_pid = body.to_i
431
+ lines = out.readlines.map!(&:chomp!)
432
+ out.close!
433
+
434
+ assert_match %r{INFO hihi from app\.atfork_child}, File.read(err.path)
435
+
436
+ assert_equal 3, lines.size
437
+ assert_equal("af #{pid} parent about to spawn", lines.shift)
438
+
439
+ # child/parent ordering is not guaranteed
440
+ assert_equal 1, lines.grep(/\Aaf #{pid} this is probably not useful\z/).size
441
+ assert_equal 1, lines.grep(/\Aaf #{worker_pid} worker is running\z/).size
442
+ ensure
443
+ quit_wait(pid)
444
+ end
445
+
446
+ def test_mp_hooks_worker_nr
447
+ err = @err
448
+ out = tmpfile(%w(mp_hooks .out))
449
+ cfg = Yahns::Config.new
450
+ host, port = @srv.addr[3], @srv.addr[1]
451
+ cfg.instance_eval do
452
+ ru = lambda {|_|x="#$$";[200,{'Content-Length'=>x.size.to_s },[x]]}
453
+ GTL.synchronize {
454
+ app(:rack, ru) {
455
+ listen "#{host}:#{port}"
456
+ persistent_connections false
457
+ atfork_child { |nr| warn "INFO hihi.#{nr} from app.atfork_child" }
458
+ }
459
+ worker_processes(1) do
460
+ atfork_child { |nr| puts "af.#{nr} #$$ worker is running" }
461
+ atfork_prepare { |nr| puts "af.#{nr} #$$ parent about to spawn" }
462
+ atfork_parent { |nr| puts "af.#{nr} #$$ this is probably not useful" }
463
+ end
464
+ }
465
+ stderr_path err.path
466
+ stdout_path out.path
467
+ end
468
+ pid = mkserver(cfg)
469
+ c = get_tcp_client(host, port)
470
+ c.write("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
471
+ buf = Timeout.timeout(10) { c.read }
472
+ c.close
473
+ head, body = buf.split(/\r\n\r\n/)
474
+ assert_match(/200 OK/, head)
475
+ assert_match(/\A\d+\z/, body)
476
+ worker_pid = body.to_i
477
+ lines = out.readlines.map!(&:chomp!)
478
+ out.close!
479
+
480
+ assert_match %r{INFO hihi\.0 from app\.atfork_child}, File.read(err.path)
481
+ assert_equal 3, lines.size
482
+ assert_equal("af.0 #{pid} parent about to spawn", lines.shift)
483
+
484
+ # child/parent ordering is not guaranteed
485
+ assert_equal 1,
486
+ lines.grep(/\Aaf\.0 #{pid} this is probably not useful\z/).size
487
+ assert_equal 1,
488
+ lines.grep(/\Aaf\.0 #{worker_pid} worker is running\z/).size
489
+ ensure
490
+ quit_wait(pid)
491
+ end
492
+
493
+ def test_pidfile_usr2
494
+ tmpdir = Dir.mktmpdir
495
+ pidf = "#{tmpdir}/pid"
496
+ old = "#{pidf}.oldbin"
497
+ err = @err
498
+ cfg = Yahns::Config.new
499
+ host, port = @srv.addr[3], @srv.addr[1]
500
+ cfg.instance_eval do
501
+ GTL.synchronize {
502
+ app(:rack, lambda { |_| [ 200, {}, [] ] }) { listen "#{host}:#{port}" }
503
+ pid pidf
504
+ }
505
+ stderr_path err.path
506
+ end
507
+ pid = mkserver(cfg) do
508
+ Yahns::START[0] = "sh"
509
+ Yahns::START[:argv] = [ '-c', "echo $$ > #{pidf}; sleep 10" ]
510
+ end
511
+
512
+ # ensure server is running
513
+ c = get_tcp_client(host, port)
514
+ c.write("GET / HTTP/1.0\r\n\r\n")
515
+ buf = Timeout.timeout(10) { c.read }
516
+ assert_match(/Connection: close/, buf)
517
+ c.close
518
+
519
+ assert_equal pid, File.read(pidf).to_i
520
+ before = File.stat(pidf)
521
+
522
+ # start the upgrade
523
+ Process.kill(:USR2, pid)
524
+ Timeout.timeout(10) { sleep(0.01) until File.exist?(old) }
525
+ after = File.stat(old)
526
+ assert_equal after.ino, before.ino
527
+ Timeout.timeout(10) { sleep(0.01) until File.exist?(pidf) }
528
+ new = File.read(pidf).to_i
529
+ refute_equal pid, new
530
+
531
+ # abort the upgrade (just wait for it to finish)
532
+ Process.kill(:TERM, new)
533
+ poke_until_dead(new)
534
+
535
+ # ensure reversion is OK
536
+ Timeout.timeout(10) { sleep(0.01) while File.exist?(old) }
537
+ after = File.stat(pidf)
538
+ assert_equal before.ino, after.ino
539
+ assert_equal before.mtime, after.mtime
540
+ assert_equal pid, File.read(pidf).to_i
541
+
542
+ lines = File.readlines(err.path).grep(/ERROR/)
543
+ assert_equal 1, lines.size
544
+ assert_match(/reaped/, lines[0], lines)
545
+ File.truncate(err.path, 0)
546
+ ensure
547
+ quit_wait(pid)
548
+ FileUtils.rm_rf(tmpdir)
549
+ end
550
+
551
+ module MockSwitchUser
552
+ def self.included(cls)
553
+ cls.__send__(:remove_method, :switch_user)
554
+ cls.__send__(:alias_method, :switch_user, :mock_switch_user)
555
+ end
556
+
557
+ def mock_switch_user(user, group = nil)
558
+ $yahns_user = [ $$, user, group ]
559
+ end
560
+ end
561
+
562
+ def test_user_no_workers
563
+ refute defined?($yahns_user), "$yahns_user global should be undefined"
564
+ err = @err
565
+ cfg = Yahns::Config.new
566
+ host, port = @srv.addr[3], @srv.addr[1]
567
+ cfg.instance_eval do
568
+ ru = lambda do |_|
569
+ b = $yahns_user.inspect
570
+ [ 200, {'Content-Length'=>b.size.to_s }, [b] ]
571
+ end
572
+ GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
573
+ user "nobody"
574
+ stderr_path err.path
575
+ end
576
+ pid = mkserver(cfg) { Yahns::Server.__send__(:include, MockSwitchUser) }
577
+ expect = [ pid, "nobody", nil ].inspect
578
+ run_client(host, port) { |res| assert_equal expect, res.body }
579
+ refute defined?($yahns_user), "$yahns_user global should be undefined"
580
+ ensure
581
+ quit_wait(pid)
582
+ end
583
+
584
+ def test_user_workers
585
+ refute defined?($yahns_user), "$yahns_user global should be undefined"
586
+ err = @err
587
+ cfg = Yahns::Config.new
588
+ host, port = @srv.addr[3], @srv.addr[1]
589
+ cfg.instance_eval do
590
+ ru = lambda do |_|
591
+ b = $yahns_user.inspect
592
+ [ 200, {'Content-Length'=>b.size.to_s, 'X-Pid' => "#$$" }, [b] ]
593
+ end
594
+ GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
595
+ user "nobody"
596
+ stderr_path err.path
597
+ worker_processes 1
598
+ end
599
+ pid = mkserver(cfg) { Yahns::Server.__send__(:include, MockSwitchUser) }
600
+ run_client(host, port) do |res|
601
+ worker_pid = res["X-Pid"].to_i
602
+ assert_operator worker_pid, :>, 0
603
+ refute_equal pid, worker_pid
604
+ refute_equal $$, worker_pid
605
+ expect = [ worker_pid, "nobody", nil ].inspect
606
+ assert_equal expect, res.body
607
+ end
608
+ refute defined?($yahns_user), "$yahns_user global should be undefined"
609
+ ensure
610
+ quit_wait(pid)
611
+ end
612
+
613
+ def test_working_directory
614
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
615
+ ru = lambda { |_|
616
+ [ 200, {'Content-Length'=>Dir.pwd.size.to_s }, [Dir.pwd] ]
617
+ }
618
+ Dir.mktmpdir do |tmpdir|
619
+ begin
620
+ pid = mkserver(cfg) do
621
+ $LOAD_PATH << File.expand_path("lib")
622
+ cfg.instance_eval do
623
+ working_directory tmpdir
624
+ app(:rack, ru) { listen "#{host}:#{port}" }
625
+ stderr_path err.path
626
+ end
627
+ end
628
+ refute_equal Dir.pwd, tmpdir
629
+ Net::HTTP.start(host, port) do |http|
630
+ assert_equal tmpdir, http.request(Net::HTTP::Get.new("/")).body
631
+ end
632
+ ensure
633
+ quit_wait(pid)
634
+ end
635
+ end
636
+ end
637
+
638
+ def test_errors
639
+ tmpdir = Dir.mktmpdir
640
+ sock = "#{tmpdir}/sock"
641
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
642
+ re = tmpfile(%w(rack .errors))
643
+ ru = lambda { |e|
644
+ e["rack.errors"].write "INFO HIHI\n"
645
+ [ 200, {'Content-Length'=>'2' }, %w(OK) ]
646
+ }
647
+ cfg.instance_eval do
648
+ GTL.synchronize {
649
+ app(:rack, ru) {
650
+ listen "#{host}:#{port}"
651
+ errors re.path
652
+ }
653
+ app(:rack, ru) { listen sock }
654
+ }
655
+ stderr_path err.path
656
+ end
657
+ pid = mkserver(cfg)
658
+ Net::HTTP.start(host, port) do |http|
659
+ assert_equal "OK", http.request(Net::HTTP::Get.new("/")).body
660
+ end
661
+ assert_equal "INFO HIHI\n", re.read
662
+
663
+ c = UNIXSocket.new(sock)
664
+ c.close_on_exec = true
665
+ c.write "GET /\r\n\r\n"
666
+ assert_equal c, c.wait(30)
667
+ assert_equal "OK", c.read
668
+ c.close
669
+ assert_match %r{INFO HIHI}, File.read(err.path)
670
+ ensure
671
+ re.close!
672
+ quit_wait(pid)
673
+ FileUtils.rm_rf(tmpdir)
674
+ end
675
+
676
+ def test_persistent_shutdown_timeout; _persistent_shutdown(nil); end
677
+ def test_persistent_shutdown_timeout_mp; _persistent_shutdown(1); end
678
+
679
+ def _persistent_shutdown(nr_workers)
680
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
681
+ pid = mkserver(cfg) do
682
+ ru = lambda { |e| [ 200, {'Content-Length'=>'2'}, %w(OK) ] }
683
+ cfg.instance_eval do
684
+ app(:rack, ru) { listen "#{host}:#{port}" }
685
+ stderr_path err.path
686
+ shutdown_timeout 1
687
+ worker_processes(nr_workers) if nr_workers
688
+ end
689
+ end
690
+ c = get_tcp_client(host, port)
691
+ c.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
692
+ assert_equal c, c.wait(30)
693
+ buf = ""
694
+ re = /\r\n\r\nOK\z/
695
+ Timeout.timeout(30) do
696
+ begin
697
+ buf << c.readpartial(666)
698
+ end until re =~ buf
699
+ end
700
+ refute_match %r{Connection: close}, buf
701
+ assert_nil c.wait(0.001), "connection should still be alive"
702
+ Process.kill(:QUIT, pid)
703
+ _, status = Timeout.timeout(5) { Process.waitpid2(pid) }
704
+ assert status.success?, status.inspect
705
+ assert_nil c.wait(1)
706
+ assert_nil c.read(666)
707
+ end
708
+
709
+ def test_before_exec
710
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
711
+ ru = lambda { |e| [ 200, {'Content-Length'=>'2' }, %w(OK) ] }
712
+ tmp = tmpfile(%w(exec .pid))
713
+ x = "echo $$ >> #{tmp.path}"
714
+ pid = mkserver(cfg) do
715
+ cfg.instance_eval do
716
+ app(:rack, ru) { listen "#{host}:#{port}" }
717
+ before_exec do |exec_cmd|
718
+ exec_cmd.replace(%W(/bin/sh -c #{x}))
719
+ end
720
+ stderr_path err.path
721
+ end
722
+ end
723
+
724
+ # did we start properly?
725
+ Net::HTTP.start(host, port) do |http|
726
+ assert_equal "OK", http.request(Net::HTTP::Get.new("/")).body
727
+ end
728
+
729
+ Process.kill(:USR2, pid)
730
+ Timeout.timeout(30) { sleep(0.01) until tmp.size > 0 }
731
+ buf = tmp.read
732
+ assert_match %r{\A\d+}, buf
733
+ exec_pid = buf.to_i
734
+ poke_until_dead exec_pid
735
+
736
+ # ensure it recovered
737
+ Net::HTTP.start(host, port) do |http|
738
+ assert_equal "OK", http.request(Net::HTTP::Get.new("/")).body
739
+ end
740
+ assert_match %r{reaped}, err.read
741
+ err.truncate(0)
742
+ ensure
743
+ tmp.close!
744
+ quit_wait(pid)
745
+ end
746
+
747
+ def test_app_controls_close
748
+ err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
749
+ pid = mkserver(cfg) do
750
+ cfg.instance_eval do
751
+ ru = lambda { |env|
752
+ h = { 'Content-Length' => '2' }
753
+ if env["PATH_INFO"] =~ %r{\A/(.+)}
754
+ h["Connection"] = $1
755
+ end
756
+ [ 200, h, ['HI'] ]
757
+ }
758
+ app(:rack, ru) { listen "#{host}:#{port}" }
759
+ stderr_path err.path
760
+ end
761
+ end
762
+ c = get_tcp_client(host, port)
763
+
764
+ # normal response
765
+ c.write "GET /keep-alive HTTP/1.1\r\nHost: example.com\r\n\r\n"
766
+ buf = ""
767
+ Timeout.timeout(30) do
768
+ buf << c.readpartial(4096) until buf =~ /HI\z/
769
+ end
770
+ assert_match %r{^Connection: keep-alive}, buf
771
+ assert_raises(Errno::EAGAIN,IO::WaitReadable) { c.read_nonblock(666) }
772
+
773
+ # we allow whatever in the response, but don't send it
774
+ c.write "GET /whatever HTTP/1.1\r\nHost: example.com\r\n\r\n"
775
+ buf = ""
776
+ Timeout.timeout(30) do
777
+ buf << c.readpartial(4096) until buf =~ /HI\z/
778
+ end
779
+ assert_match %r{^Connection: keep-alive}, buf
780
+ assert_raises(Errno::EAGAIN,IO::WaitReadable) { c.read_nonblock(666) }
781
+
782
+ c.write "GET /close HTTP/1.1\r\nHost: example.com\r\n\r\n"
783
+ buf = ""
784
+ Timeout.timeout(30) do
785
+ buf << c.readpartial(4096) until buf =~ /HI\z/
786
+ end
787
+ assert_match %r{^Connection: close}, buf
788
+ assert_equal c, IO.select([c], nil, nil, 30)[0][0]
789
+ assert_raises(EOFError) { c.readpartial(666) }
790
+ c.close
791
+ ensure
792
+ quit_wait(pid)
793
+ end
419
794
  end