yahns 0.0.0TP1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/COPYING +674 -0
  4. data/GIT-VERSION-GEN +41 -0
  5. data/GNUmakefile +90 -0
  6. data/README +127 -0
  7. data/Rakefile +60 -0
  8. data/bin/yahns +32 -0
  9. data/examples/README +3 -0
  10. data/examples/init.sh +76 -0
  11. data/examples/logger_mp_safe.rb +28 -0
  12. data/examples/logrotate.conf +32 -0
  13. data/examples/yahns_multi.conf.rb +89 -0
  14. data/examples/yahns_rack_basic.conf.rb +27 -0
  15. data/lib/yahns.rb +73 -0
  16. data/lib/yahns/acceptor.rb +28 -0
  17. data/lib/yahns/client_expire.rb +40 -0
  18. data/lib/yahns/client_expire_portable.rb +39 -0
  19. data/lib/yahns/config.rb +344 -0
  20. data/lib/yahns/daemon.rb +51 -0
  21. data/lib/yahns/fdmap.rb +90 -0
  22. data/lib/yahns/http_client.rb +198 -0
  23. data/lib/yahns/http_context.rb +65 -0
  24. data/lib/yahns/http_response.rb +184 -0
  25. data/lib/yahns/log.rb +73 -0
  26. data/lib/yahns/queue.rb +7 -0
  27. data/lib/yahns/queue_egg.rb +23 -0
  28. data/lib/yahns/queue_epoll.rb +57 -0
  29. data/lib/yahns/rack.rb +80 -0
  30. data/lib/yahns/server.rb +336 -0
  31. data/lib/yahns/server_mp.rb +181 -0
  32. data/lib/yahns/sigevent.rb +7 -0
  33. data/lib/yahns/sigevent_efd.rb +18 -0
  34. data/lib/yahns/sigevent_pipe.rb +29 -0
  35. data/lib/yahns/socket_helper.rb +117 -0
  36. data/lib/yahns/stream_file.rb +34 -0
  37. data/lib/yahns/stream_input.rb +150 -0
  38. data/lib/yahns/tee_input.rb +114 -0
  39. data/lib/yahns/tmpio.rb +27 -0
  40. data/lib/yahns/wbuf.rb +36 -0
  41. data/lib/yahns/wbuf_common.rb +32 -0
  42. data/lib/yahns/worker.rb +58 -0
  43. data/test/covshow.rb +29 -0
  44. data/test/helper.rb +115 -0
  45. data/test/server_helper.rb +65 -0
  46. data/test/test_bin.rb +97 -0
  47. data/test/test_client_expire.rb +132 -0
  48. data/test/test_config.rb +56 -0
  49. data/test/test_fdmap.rb +19 -0
  50. data/test/test_output_buffering.rb +291 -0
  51. data/test/test_queue.rb +59 -0
  52. data/test/test_rack.rb +28 -0
  53. data/test/test_serve_static.rb +42 -0
  54. data/test/test_server.rb +415 -0
  55. data/test/test_stream_file.rb +30 -0
  56. data/test/test_wbuf.rb +136 -0
  57. data/yahns.gemspec +19 -0
  58. metadata +165 -0
@@ -0,0 +1,59 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative 'helper'
4
+ require 'timeout'
5
+ require 'stringio'
6
+
7
+ class TestQueue < Testcase
8
+ parallelize_me!
9
+
10
+ def setup
11
+ @q = Yahns::Queue.new
12
+ @err = StringIO.new
13
+ @logger = Logger.new(@err)
14
+ @q.fdmap = @fdmap = Yahns::Fdmap.new(@logger, 0.5)
15
+ assert @q.close_on_exec?
16
+ end
17
+
18
+ def test_queue
19
+ r, w = IO.pipe
20
+ assert_equal 0, @fdmap.size
21
+ @q.queue_add(r, Yahns::Queue::QEV_RD)
22
+ assert_equal 1, @fdmap.size
23
+ def r.yahns_step
24
+ begin
25
+ case read_nonblock(11)
26
+ when "delete"
27
+ return :delete
28
+ end
29
+ rescue Errno::EAGAIN
30
+ return :wait_readable
31
+ rescue EOFError
32
+ return nil
33
+ end while true
34
+ end
35
+ w.write('.')
36
+ Timeout.timeout(10) do
37
+ Thread.pass until r.nread > 0
38
+ @q.spawn_worker_threads(@logger, 1, 1)
39
+ Thread.pass until r.nread == 0
40
+
41
+ w.write("delete")
42
+ Thread.pass until r.nread == 0
43
+ Thread.pass until @fdmap.size == 0
44
+
45
+ # should not raise
46
+ @q.queue_add(r, Yahns::Queue::QEV_RD)
47
+ assert_equal 1, @fdmap.size
48
+ w.close
49
+ Thread.pass until @fdmap.size == 0
50
+ end
51
+ assert r.closed?
52
+ ensure
53
+ [ r, w ].each { |io| io.close unless io.closed? }
54
+ end
55
+
56
+ def teardown
57
+ @q.close
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative 'helper'
4
+ require 'rack/lobster'
5
+ require 'yahns/rack'
6
+ class TestRack < Testcase
7
+ parallelize_me!
8
+
9
+ def test_rack
10
+ tmp = tmpfile(%W(config .ru))
11
+ tmp.write "run Rack::Lobster.new\n"
12
+ rapp = GTL.synchronize { Yahns::Rack.new(tmp.path) }
13
+ assert_kind_of Rack::Lobster, GTL.synchronize { rapp.app_after_fork }
14
+ defaults = rapp.app_defaults
15
+ assert_kind_of Hash, defaults
16
+ tmp.close!
17
+ end
18
+
19
+ def test_rack_preload
20
+ tmp = tmpfile(%W(config .ru))
21
+ tmp.write "run Rack::Lobster.new\n"
22
+ rapp = GTL.synchronize { Yahns::Rack.new(tmp.path, preload: true) }
23
+ assert_kind_of Rack::Lobster, rapp.instance_variable_get(:@app)
24
+ defaults = rapp.app_defaults
25
+ assert_kind_of Hash, defaults
26
+ tmp.close!
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative 'server_helper'
4
+ require 'rack/file'
5
+
6
+ class TestServeStatic < Testcase
7
+ parallelize_me!
8
+ include ServerHelper
9
+ alias setup server_helper_setup
10
+ alias teardown server_helper_teardown
11
+
12
+ def test_serve_static
13
+ err = @err
14
+ cfg = Yahns::Config.new
15
+ host, port = @srv.addr[3], @srv.addr[1]
16
+ cfg.instance_eval do
17
+ GTL.synchronize do
18
+ app(:rack, Rack::File.new(Dir.pwd)) { listen "#{host}:#{port}" }
19
+ end
20
+ logger(Logger.new(err.path))
21
+ end
22
+ srv = Yahns::Server.new(cfg)
23
+ pid = fork do
24
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
25
+ srv.start.join
26
+ end
27
+ gplv3 = File.read("COPYING")
28
+ Net::HTTP.start(host, port) do |http|
29
+ res = http.request(Net::HTTP::Get.new("/COPYING"))
30
+ assert_equal gplv3, res.body
31
+
32
+ req = Net::HTTP::Get.new("/COPYING", "Range" => "bytes=5-46")
33
+ res = http.request(req)
34
+ assert_equal gplv3[5..46], res.body
35
+ end
36
+ rescue => e
37
+ Yahns::Log.exception(Logger.new($stderr), "test", e)
38
+ raise
39
+ ensure
40
+ quit_wait(pid)
41
+ end
42
+ end
@@ -0,0 +1,415 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative 'server_helper'
4
+
5
+ class TestServer < Testcase
6
+ parallelize_me!
7
+ include ServerHelper
8
+
9
+ alias setup server_helper_setup
10
+ alias teardown server_helper_teardown
11
+
12
+ def test_single_process
13
+ err = @err
14
+ cfg = Yahns::Config.new
15
+ host, port = @srv.addr[3], @srv.addr[1]
16
+ cfg.instance_eval do
17
+ ru = lambda { |_| [ 200, {'Content-Length'=>'2'}, ['HI'] ] }
18
+ GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
19
+ logger(Logger.new(err.path))
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
26
+ run_client(host, port) { |res| assert_equal "HI", res.body }
27
+ c = TCPSocket.new(host, port)
28
+
29
+ # test pipelining
30
+ r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
31
+ c.write(r + r)
32
+ buf = ""
33
+ Timeout.timeout(10) do
34
+ until buf =~ /HI.+HI/m
35
+ buf << c.readpartial(4096)
36
+ end
37
+ end
38
+
39
+ # trickle pipelining
40
+ c.write(r + "GET ")
41
+ buf = ""
42
+ Timeout.timeout(10) do
43
+ until buf =~ /HI\z/
44
+ buf << c.readpartial(4096)
45
+ end
46
+ end
47
+ c.write("/ HTTP/1.1\r\nHost: example.com\r\n\r\n")
48
+ Timeout.timeout(10) do
49
+ until buf =~ /HI.+HI/m
50
+ buf << c.readpartial(4096)
51
+ end
52
+ end
53
+ Process.kill(:QUIT, pid)
54
+ "GET / HTTP/1.1\r\n\r\n".each_byte { |x| Thread.pass; c.write(x.chr) }
55
+ buf = Timeout.timeout(10) { c.read }
56
+ assert_match(/Connection: close/, buf)
57
+ _, status = Timeout.timeout(10) { Process.waitpid2(pid) }
58
+ assert status.success?, status.inspect
59
+ c.close
60
+ rescue => e
61
+ Yahns::Log.exception(Logger.new($stderr), "test", e)
62
+ raise
63
+ end
64
+
65
+ def test_input_body_true; input_body(true); end
66
+ def test_input_body_false; input_body(false); end
67
+ def test_input_body_lazy; input_body(:lazy); end
68
+
69
+ def input_body(btype)
70
+ err = @err
71
+ cfg = Yahns::Config.new
72
+ host, port = @srv.addr[3], @srv.addr[1]
73
+ cfg.instance_eval do
74
+ ru = lambda {|e|[ 200, {'Content-Length'=>'2'},[e["rack.input"].read]]}
75
+ GTL.synchronize do
76
+ app(:rack, ru) do
77
+ listen "#{host}:#{port}"
78
+ input_buffering btype
79
+ end
80
+ end
81
+ logger(Logger.new(err.path))
82
+ end
83
+ srv = Yahns::Server.new(cfg)
84
+ pid = fork do
85
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
86
+ srv.start.join
87
+ end
88
+ c = TCPSocket.new(host, port)
89
+ buf = "PUT / HTTP/1.0\r\nContent-Length: 2\r\n\r\nHI"
90
+ c.write(buf)
91
+ IO.select([c], nil, nil, 5)
92
+ rv = c.read(666)
93
+ head, body = rv.split(/\r\n\r\n/)
94
+ assert_match(%r{^Content-Length: 2\r\n}, head)
95
+ assert_equal "HI", body, "#{rv.inspect} - #{btype.inspect}"
96
+ c.close
97
+
98
+ # pipelined oneshot
99
+ buf = "PUT / HTTP/1.1\r\nContent-Length: 2\r\n\r\nHI"
100
+ c = TCPSocket.new(host, port)
101
+ c.write(buf + buf)
102
+ buf = ""
103
+ Timeout.timeout(10) do
104
+ until buf =~ /HI.+HI/m
105
+ buf << c.readpartial(4096)
106
+ end
107
+ end
108
+ assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date"
109
+ rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHI)/m, "")
110
+ first = $1
111
+ assert rv
112
+ assert_equal first, buf
113
+
114
+ # pipelined trickle
115
+ buf = "PUT / HTTP/1.1\r\nContent-Length: 5\r\n\r\nHIBYE"
116
+ (buf + buf).each_byte do |b|
117
+ c.write(b.chr)
118
+ sleep(0.01) if b.chr == ":"
119
+ Thread.pass
120
+ end
121
+ buf = ""
122
+ Timeout.timeout(10) do
123
+ until buf =~ /HIBYE.+HIBYE/m
124
+ buf << c.readpartial(4096)
125
+ end
126
+ end
127
+ assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date"
128
+ rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHIBYE)/m, "")
129
+ first = $1
130
+ assert rv
131
+ assert_equal first, buf
132
+ rescue => e
133
+ Yahns::Log.exception(Logger.new($stderr), "test", e)
134
+ raise
135
+ ensure
136
+ c.close if c
137
+ quit_wait(pid)
138
+ end
139
+
140
+ def test_trailer_true; trailer(true); end
141
+ def test_trailer_false; trailer(false); end
142
+ def test_trailer_lazy; trailer(:lazy); end
143
+ def test_slow_trailer_true; trailer(true, 0.02); end
144
+ def test_slow_trailer_false; trailer(false, 0.02); end
145
+ def test_slow_trailer_lazy; trailer(:lazy, 0.02); end
146
+
147
+ def trailer(btype, delay = false)
148
+ err = @err
149
+ cfg = Yahns::Config.new
150
+ host, port = @srv.addr[3], @srv.addr[1]
151
+ cfg.instance_eval do
152
+ ru = lambda do |e|
153
+ body = e["rack.input"].read
154
+ s = e["HTTP_XBT"] + "\n" + body
155
+ [ 200, {'Content-Length'=>s.size.to_s}, [ s ] ]
156
+ end
157
+ GTL.synchronize do
158
+ app(:rack, ru) do
159
+ listen "#{host}:#{port}"
160
+ input_buffering btype
161
+ end
162
+ end
163
+ logger(Logger.new(err.path))
164
+ end
165
+ srv = Yahns::Server.new(cfg)
166
+ pid = fork do
167
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
168
+ srv.start.join
169
+ end
170
+ c = TCPSocket.new(host, port)
171
+ buf = "PUT / HTTP/1.0\r\nTrailer:xbt\r\nTransfer-Encoding: chunked\r\n\r\n"
172
+ c.write(buf)
173
+ xbt = btype.to_s
174
+ sleep(delay) if delay
175
+ c.write(sprintf("%x\r\n", xbt.size))
176
+ sleep(delay) if delay
177
+ c.write(xbt)
178
+ sleep(delay) if delay
179
+ c.write("\r\n")
180
+ sleep(delay) if delay
181
+ c.write("0\r\nXBT: ")
182
+ sleep(delay) if delay
183
+ c.write("#{xbt}\r\n\r\n")
184
+ IO.select([c], nil, nil, 5000) or raise "timed out"
185
+ rv = c.read(666)
186
+ _, body = rv.split(/\r\n\r\n/)
187
+ a, b = body.split(/\n/)
188
+ assert_equal xbt, a
189
+ assert_equal xbt, b
190
+ ensure
191
+ c.close if c
192
+ quit_wait(pid)
193
+ end
194
+
195
+ def test_check_client_connection
196
+ msgs = %w(ZZ zz)
197
+ err = @err
198
+ cfg = Yahns::Config.new
199
+ bpipe = IO.pipe
200
+ host, port = @srv.addr[3], @srv.addr[1]
201
+ cfg.instance_eval do
202
+ ru = lambda { |e|
203
+ case e['PATH_INFO']
204
+ when '/sleep'
205
+ a = Object.new
206
+ a.instance_variable_set(:@bpipe, bpipe)
207
+ a.instance_variable_set(:@msgs, msgs)
208
+ def a.each
209
+ @msgs.each do |msg|
210
+ yield @bpipe[0].read(msg.size)
211
+ end
212
+ end
213
+ when '/cccfail'
214
+ # we should not get here if check_client_connection worked
215
+ abort "CCCFAIL"
216
+ else
217
+ a = %w(HI)
218
+ end
219
+ [ 200, {'Content-Length'=>'2'}, a ]
220
+ }
221
+ GTL.synchronize {
222
+ app(:rack, ru) {
223
+ listen "#{host}:#{port}"
224
+ check_client_connection true
225
+ # needed to avoid concurrency with check_client_connection
226
+ queue { worker_threads 1 }
227
+ output_buffering false
228
+ }
229
+ }
230
+ logger(Logger.new(err.path))
231
+ end
232
+ srv = Yahns::Server.new(cfg)
233
+
234
+ # ensure we set worker_threads correctly
235
+ eggs = srv.instance_variable_get(:@config).qeggs
236
+ assert_equal 1, eggs.size
237
+ assert_equal 1, eggs[:default].instance_variable_get(:@worker_threads)
238
+
239
+ pid = fork do
240
+ bpipe[1].close
241
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
242
+ srv.start.join
243
+ end
244
+ bpipe[0].close
245
+ a = TCPSocket.new(host, port)
246
+ b = TCPSocket.new(host, port)
247
+ a.write("GET /sleep HTTP/1.0\r\n\r\n")
248
+ r = IO.select([a], nil, nil, 4)
249
+ assert r, "nothing ready"
250
+ assert_equal a, r[0][0]
251
+ buf = a.read(8)
252
+ assert_equal "HTTP/1.1", buf
253
+
254
+ # hope the kernel sees this before it sees the bpipe ping-ponging below
255
+ b.write("GET /cccfail HTTP/1.0\r\n\r\n")
256
+ b.shutdown
257
+ b.close
258
+
259
+ # ping-pong a bit to stall the server
260
+ msgs.each do |msg|
261
+ bpipe[1].write(msg)
262
+ Timeout.timeout(10) { buf << a.readpartial(10) until buf =~ /#{msg}/ }
263
+ end
264
+ bpipe[1].close
265
+ assert_equal msgs.join, buf.split(/\r\n\r\n/)[1]
266
+
267
+ # do things still work?
268
+ run_client(host, port) { |res| assert_equal "HI", res.body }
269
+ a.close
270
+ ensure
271
+ quit_wait(pid)
272
+ end
273
+
274
+ def test_mp
275
+ pid, host, port = new_mp_server
276
+ wpid = nil
277
+ run_client(host, port) do |res|
278
+ wpid ||= res.body.to_i
279
+ end
280
+ ensure
281
+ quit_wait(pid)
282
+ if wpid
283
+ assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid) }
284
+ assert_raises(Errno::ECHILD) { Process.waitpid2(wpid) }
285
+ end
286
+ end
287
+
288
+ # Linux blocking accept() has fair behavior between multiple tasks
289
+ def test_mp_balance
290
+ skip("linux-only test") unless RUBY_PLATFORM =~ /linux/
291
+ pid, host, port = new_mp_server(2)
292
+ seen = {}
293
+
294
+ # wait for both processes to spin up
295
+ Timeout.timeout(10) do
296
+ run_client(host, port) { |res| seen[res.body] = 1 } until seen.size == 2
297
+ end
298
+
299
+ prev = nil
300
+ req = Net::HTTP::Get.new("/")
301
+ # we should bounce new connections between 2 processes
302
+ 4.times do
303
+ Net::HTTP.start(host, port) do |http|
304
+ res = http.request(req)
305
+ assert_equal 200, res.code.to_i
306
+ assert_equal "keep-alive", res["Connection"]
307
+ refute_equal prev, res.body, "same PID accepted twice"
308
+ prev = res.body.dup
309
+ seen[prev] += 1
310
+ 666.times { Thread.pass } # have the other acceptor to wake up
311
+ end
312
+ end
313
+ assert_equal 2, seen.size
314
+ ensure
315
+ quit_wait(pid)
316
+ end
317
+
318
+ def test_mp_worker_die
319
+ pid, host, port = new_mp_server
320
+ wpid1 = wpid2 = nil
321
+ run_client(host, port) do |res|
322
+ wpid1 ||= res.body.to_i
323
+ end
324
+ Process.kill(:QUIT, wpid1)
325
+ poke_until_dead(wpid1)
326
+ run_client(host, port) do |res|
327
+ wpid2 ||= res.body.to_i
328
+ end
329
+ refute_equal wpid2, wpid1
330
+ ensure
331
+ quit_wait(pid)
332
+ assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid2) } if wpid2
333
+ end
334
+
335
+ def test_mp_dead_parent
336
+ pid, host, port = new_mp_server
337
+ wpid = nil
338
+ run_client(host, port) do |res|
339
+ wpid ||= res.body.to_i
340
+ end
341
+ Process.kill(:KILL, pid)
342
+ _, status = Process.waitpid2(pid)
343
+ assert status.signaled?, status.inspect
344
+ poke_until_dead(wpid)
345
+ end
346
+
347
+ def run_client(host, port)
348
+ c = TCPSocket.new(host, port)
349
+ Net::HTTP.start(host, port) do |http|
350
+ res = http.request(Net::HTTP::Get.new("/"))
351
+ assert_equal 200, res.code.to_i
352
+ assert_equal "keep-alive", res["Connection"]
353
+ yield res
354
+ res = http.request(Net::HTTP::Get.new("/"))
355
+ assert_equal 200, res.code.to_i
356
+ assert_equal "keep-alive", res["Connection"]
357
+ yield res
358
+ end
359
+ c.write "GET / HTTP/1.0\r\n\r\n"
360
+ res = Timeout.timeout(10) { c.read }
361
+ head, _ = res.split(/\r\n\r\n/)
362
+ head = head.split(/\r\n/)
363
+ assert_equal "HTTP/1.1 200 OK", head[0]
364
+ assert_equal "Connection: close", head[-1]
365
+ c.close
366
+ end
367
+
368
+ def new_mp_server(nr = 1)
369
+ ru = @ru = tmpfile(%w(config .ru))
370
+ @ru.puts('a = $$.to_s')
371
+ @ru.puts('run lambda { |_| [ 200, {"Content-Length"=>a.size.to_s},[a]]}')
372
+ err = @err
373
+ cfg = Yahns::Config.new
374
+ host, port = @srv.addr[3], @srv.addr[1]
375
+ cfg.instance_eval do
376
+ worker_processes 2
377
+ GTL.synchronize { app(:rack, ru.path) { listen "#{host}:#{port}" } }
378
+ logger(Logger.new(File.open(err.path, "a")))
379
+ end
380
+ srv = Yahns::Server.new(cfg)
381
+ pid = fork do
382
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
383
+ srv.start.join
384
+ end
385
+ [ pid, host, port ]
386
+ end
387
+
388
+ def test_nonpersistent
389
+ err = @err
390
+ cfg = Yahns::Config.new
391
+ host, port = @srv.addr[3], @srv.addr[1]
392
+ cfg.instance_eval do
393
+ ru = lambda { |_| [ 200, {'Content-Length'=>'2'}, ['HI'] ] }
394
+ GTL.synchronize {
395
+ app(:rack, ru) {
396
+ listen "#{host}:#{port}"
397
+ persistent_connections false
398
+ }
399
+ }
400
+ logger(Logger.new(err.path))
401
+ end
402
+ srv = Yahns::Server.new(cfg)
403
+ pid = fork do
404
+ ENV["YAHNS_FD"] = @srv.fileno.to_s
405
+ srv.start.join
406
+ end
407
+ c = TCPSocket.new(host, port)
408
+ c.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
409
+ buf = Timeout.timeout(10) { c.read }
410
+ assert_match(/Connection: close/, buf)
411
+ c.close
412
+ ensure
413
+ quit_wait(pid)
414
+ end
415
+ end