yahns 0.0.0TP1

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 (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