yahns 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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