yahns 1.6.0 → 1.7.0
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.
- checksums.yaml +4 -4
- data/Documentation/yahns_config.txt +3 -0
- data/GIT-VERSION-GEN +1 -1
- data/extras/proxy_pass.rb +22 -16
- data/lib/yahns/client_expire_tcpi.rb +1 -1
- data/lib/yahns/config.rb +4 -5
- data/lib/yahns/fdmap.rb +9 -0
- data/lib/yahns/http_client.rb +19 -19
- data/lib/yahns/http_context.rb +11 -18
- data/lib/yahns/http_response.rb +2 -2
- data/lib/yahns/openssl_client.rb +26 -6
- data/lib/yahns/proxy_http_response.rb +293 -0
- data/lib/yahns/proxy_pass.rb +248 -0
- data/lib/yahns/queue_epoll.rb +7 -13
- data/lib/yahns/queue_kqueue.rb +7 -8
- data/lib/yahns/rackup_handler.rb +0 -1
- data/lib/yahns/socket_helper.rb +2 -2
- data/lib/yahns/tee_input.rb +1 -1
- data/lib/yahns/tmpio.rb +6 -2
- data/lib/yahns/wbuf.rb +29 -13
- data/test/helper.rb +10 -0
- data/test/test_extras_proxy_pass.rb +3 -0
- data/test/test_input.rb +50 -1
- data/test/test_proxy_pass.rb +611 -0
- data/test/test_rack_hijack.rb +14 -10
- data/test/test_server.rb +3 -1
- data/test/test_ssl.rb +72 -0
- data/test/test_tmpio.rb +20 -0
- data/test/test_wbuf.rb +4 -3
- metadata +6 -2
@@ -0,0 +1,248 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013-2015 all contributors <yahns-public@yhbt.net>
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
require 'socket'
|
5
|
+
require 'kgio'
|
6
|
+
require 'kcar' # gem install kcar
|
7
|
+
require 'rack/request'
|
8
|
+
require 'timeout'
|
9
|
+
|
10
|
+
require_relative 'proxy_http_response'
|
11
|
+
|
12
|
+
class Yahns::ProxyPass # :nodoc:
|
13
|
+
class ReqRes < Kgio::Socket
|
14
|
+
attr_writer :resbuf
|
15
|
+
attr_accessor :proxy_trailers
|
16
|
+
|
17
|
+
def req_start(c, req, input, chunked)
|
18
|
+
@hdr = @resbuf = nil
|
19
|
+
@yahns_client = c
|
20
|
+
@rrstate = input ? [ req, input, chunked ] : req
|
21
|
+
Thread.current[:yahns_queue].queue_add(self, Yahns::Queue::QEV_WR)
|
22
|
+
end
|
23
|
+
|
24
|
+
# we must reinitialize the thread-local rbuf if it may get beyond the
|
25
|
+
# current thread
|
26
|
+
def detach_rbuf!
|
27
|
+
Thread.current[:yahns_rbuf] = ''
|
28
|
+
end
|
29
|
+
|
30
|
+
def yahns_step # yahns event loop entry point
|
31
|
+
c = @yahns_client
|
32
|
+
case req = @rrstate
|
33
|
+
when Kcar::Parser # reading response...
|
34
|
+
buf = Thread.current[:yahns_rbuf]
|
35
|
+
|
36
|
+
case resbuf = @resbuf # where are we at the response?
|
37
|
+
when nil # common case, catch the response header in a single read
|
38
|
+
|
39
|
+
case rv = kgio_tryread(0x2000, buf)
|
40
|
+
when String
|
41
|
+
if res = req.headers(@hdr = [], rv)
|
42
|
+
return c.proxy_response_start(res, rv, req, self)
|
43
|
+
else # ugh, big headers or tricked response
|
44
|
+
buf = detach_rbuf!
|
45
|
+
@resbuf = rv
|
46
|
+
end
|
47
|
+
# continue looping in middle "case @resbuf" loop
|
48
|
+
when :wait_readable
|
49
|
+
return rv # spurious wakeup
|
50
|
+
when nil then return c.proxy_err_response(502, self, nil, nil)
|
51
|
+
end # NOT looping here
|
52
|
+
|
53
|
+
when String # continue reading trickled response headers from upstream
|
54
|
+
|
55
|
+
case rv = kgio_tryread(0x2000, buf)
|
56
|
+
when String then res = req.headers(@hdr, resbuf << rv) and break
|
57
|
+
when :wait_readable then return rv
|
58
|
+
when nil then return c.proxy_err_response(502, self, nil, nil)
|
59
|
+
end while true
|
60
|
+
|
61
|
+
return c.proxy_response_start(res, resbuf, req, self)
|
62
|
+
|
63
|
+
when Yahns::WbufCommon # streaming/buffering the response body
|
64
|
+
|
65
|
+
return c.proxy_response_finish(req, resbuf, self)
|
66
|
+
|
67
|
+
end while true # case @resbuf
|
68
|
+
|
69
|
+
when Array # [ (str|vec), rack.input, chunked? ]
|
70
|
+
send_req_body(req) # returns nil or :wait_writable
|
71
|
+
when String # buffered request header
|
72
|
+
send_req_buf(req)
|
73
|
+
end
|
74
|
+
rescue => e
|
75
|
+
# avoid polluting logs with a giant backtrace when the problem isn't
|
76
|
+
# fixable in code.
|
77
|
+
e.set_backtrace([]) if Errno::ECONNREFUSED === e
|
78
|
+
c.proxy_err_response(502, self, e, nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
# returns :wait_readable if complete, :wait_writable if not
|
82
|
+
def send_req_body(req)
|
83
|
+
buf, input, chunked = req
|
84
|
+
|
85
|
+
# get the first buffered chunk or vector
|
86
|
+
case rv = String === buf ? kgio_trywrite(buf) : kgio_trywritev(buf)
|
87
|
+
when String, Array
|
88
|
+
buf = rv # retry inner loop
|
89
|
+
when :wait_writable
|
90
|
+
req[0] = buf
|
91
|
+
return :wait_writable
|
92
|
+
when nil
|
93
|
+
break # onto writing body
|
94
|
+
end while true
|
95
|
+
|
96
|
+
buf = Thread.current[:yahns_rbuf]
|
97
|
+
|
98
|
+
# Note: input (env['rack.input']) is fully-buffered by default so
|
99
|
+
# we should not be waiting on a slow network resource when reading
|
100
|
+
# input. However, some weird configs may disable this on LANs
|
101
|
+
|
102
|
+
if chunked
|
103
|
+
while input.read(0x2000, buf)
|
104
|
+
vec = [ "#{buf.size.to_s(16)}\r\n", buf, "\r\n".freeze ]
|
105
|
+
case rv = kgio_trywritev(vec)
|
106
|
+
when Array
|
107
|
+
vec = rv # partial write, retry in case loop
|
108
|
+
when :wait_writable
|
109
|
+
detach_rbuf!
|
110
|
+
req[0] = vec
|
111
|
+
return :wait_writable
|
112
|
+
when nil
|
113
|
+
break # continue onto reading next chunk
|
114
|
+
end while true
|
115
|
+
end
|
116
|
+
close_req_body(input)
|
117
|
+
|
118
|
+
# note: we do not send any trailer, they are folded into the header
|
119
|
+
# because this relies on full request buffering
|
120
|
+
send_req_buf("0\r\n\r\n".freeze)
|
121
|
+
# prepare_wait_readable already called by send_req_buf
|
122
|
+
else # identity request, easy:
|
123
|
+
while input.read(0x2000, buf)
|
124
|
+
case rv = kgio_trywrite(buf)
|
125
|
+
when String
|
126
|
+
buf = rv # partial write, retry in case loop
|
127
|
+
when :wait_writable
|
128
|
+
detach_rbuf!
|
129
|
+
req[0] = buf
|
130
|
+
return :wait_writable
|
131
|
+
when nil
|
132
|
+
break # continue onto reading next block
|
133
|
+
end while true
|
134
|
+
end
|
135
|
+
|
136
|
+
close_req_body(input)
|
137
|
+
prepare_wait_readable
|
138
|
+
end
|
139
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN
|
140
|
+
# no more reading off the client socket, just prepare to forward
|
141
|
+
# the rejection response from the upstream (if any)
|
142
|
+
@yahns_client.to_io.shutdown(Socket::SHUT_RD)
|
143
|
+
prepare_wait_readable
|
144
|
+
end
|
145
|
+
|
146
|
+
def prepare_wait_readable
|
147
|
+
@rrstate = Kcar::Parser.new
|
148
|
+
:wait_readable # all done sending the request, wait for response
|
149
|
+
end
|
150
|
+
|
151
|
+
def close_req_body(input)
|
152
|
+
case input
|
153
|
+
when Yahns::TeeInput, IO, StringIO
|
154
|
+
input.close
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# n.b. buf must be a detached string not shared with
|
159
|
+
# Thread.current[:yahns_rbuf] of any thread
|
160
|
+
def send_req_buf(buf)
|
161
|
+
case rv = kgio_trywrite(buf)
|
162
|
+
when String
|
163
|
+
buf = rv # retry inner loop
|
164
|
+
when :wait_writable
|
165
|
+
@rrstate = buf
|
166
|
+
return :wait_writable
|
167
|
+
when nil
|
168
|
+
return prepare_wait_readable
|
169
|
+
end while true
|
170
|
+
end
|
171
|
+
end # class ReqRes
|
172
|
+
|
173
|
+
def initialize(dest)
|
174
|
+
case dest
|
175
|
+
when %r{\Aunix:([^:]+)(?::(/.*))?\z}
|
176
|
+
path = $2
|
177
|
+
@sockaddr = Socket.sockaddr_un($1)
|
178
|
+
when %r{\Ahttp://([^/]+)(/.*)?\z}
|
179
|
+
path = $2
|
180
|
+
host, port = $1.split(':')
|
181
|
+
@sockaddr = Socket.sockaddr_in(port || 80, host)
|
182
|
+
else
|
183
|
+
raise ArgumentError, "destination must be an HTTP URL or unix: path"
|
184
|
+
end
|
185
|
+
init_path_vars(path)
|
186
|
+
end
|
187
|
+
|
188
|
+
def init_path_vars(path)
|
189
|
+
path ||= '$fullpath'
|
190
|
+
# methods from Rack::Request we want:
|
191
|
+
allow = %w(fullpath host_with_port host port url path)
|
192
|
+
want = path.scan(/\$(\w+)/).flatten! || []
|
193
|
+
diff = want - allow
|
194
|
+
diff.empty? or
|
195
|
+
raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
|
196
|
+
|
197
|
+
# kill leading slash just in case...
|
198
|
+
@path = path.gsub(%r{\A/(\$(?:fullpath|path))}, '\1')
|
199
|
+
end
|
200
|
+
|
201
|
+
def call(env)
|
202
|
+
# 3-way handshake for TCP backends while we generate the request header
|
203
|
+
rr = ReqRes.start(@sockaddr)
|
204
|
+
c = env['rack.hijack'].call
|
205
|
+
|
206
|
+
req = Rack::Request.new(env)
|
207
|
+
req = @path.gsub(/\$(\w+)/) { req.__send__($1) }
|
208
|
+
|
209
|
+
# start the connection asynchronously and early so TCP can do a
|
210
|
+
case ver = env['HTTP_VERSION']
|
211
|
+
when 'HTTP/1.1' # leave alone, response may be chunked
|
212
|
+
else # no chunking for HTTP/1.0 and HTTP/0.9
|
213
|
+
ver = 'HTTP/1.0'.freeze
|
214
|
+
end
|
215
|
+
|
216
|
+
req = "#{env['REQUEST_METHOD']} #{req} #{ver}\r\n" \
|
217
|
+
"X-Forwarded-For: #{env["REMOTE_ADDR"]}\r\n"
|
218
|
+
|
219
|
+
# pass most HTTP_* headers through as-is
|
220
|
+
chunked = false
|
221
|
+
env.each do |key, val|
|
222
|
+
%r{\AHTTP_(\w+)\z} =~ key or next
|
223
|
+
key = $1
|
224
|
+
# trailers are folded into the header, so do not send the Trailer:
|
225
|
+
# header in the request
|
226
|
+
next if /\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR|TRAILER)/ =~
|
227
|
+
key
|
228
|
+
'TRANSFER_ENCODING'.freeze == key && val =~ /\bchunked\b/i and
|
229
|
+
chunked = true
|
230
|
+
key.tr!('_'.freeze, '-'.freeze)
|
231
|
+
req << "#{key}: #{val}\r\n"
|
232
|
+
end
|
233
|
+
|
234
|
+
# special cases which Rack does not prefix:
|
235
|
+
ctype = env["CONTENT_TYPE"] and req << "Content-Type: #{ctype}\r\n"
|
236
|
+
clen = env["CONTENT_LENGTH"] and req << "Content-Length: #{clen}\r\n"
|
237
|
+
input = chunked || (clen && clen.to_i > 0) ? env['rack.input'] : nil
|
238
|
+
|
239
|
+
# finally, prepare to emit the headers
|
240
|
+
rr.req_start(c, req << "\r\n".freeze, input, chunked)
|
241
|
+
|
242
|
+
# this probably breaks fewer middlewares than returning whatever else...
|
243
|
+
[ 500, [], [] ]
|
244
|
+
rescue => e
|
245
|
+
Yahns::Log.exception(env['rack.logger'], 'proxy_pass', e)
|
246
|
+
[ 502, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
|
247
|
+
end
|
248
|
+
end
|
data/lib/yahns/queue_epoll.rb
CHANGED
@@ -27,20 +27,14 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
27
27
|
epoll_ctl(Epoll::CTL_ADD, io, flags)
|
28
28
|
end
|
29
29
|
|
30
|
+
def queue_mod(io, flags)
|
31
|
+
epoll_ctl(Epoll::CTL_MOD, io, flags)
|
32
|
+
end
|
33
|
+
|
30
34
|
def thr_init
|
31
35
|
Thread.current[:yahns_rbuf] = ""
|
32
36
|
Thread.current[:yahns_fdmap] = @fdmap
|
33
|
-
|
34
|
-
|
35
|
-
# use only before hijacking, once hijacked, io may be unusable to us
|
36
|
-
# It is not safe to call this unless it is an unarmed EPOLLONESHOT
|
37
|
-
# object.
|
38
|
-
def queue_del(io)
|
39
|
-
# order does not really matter here, however Epoll::CTL_DEL
|
40
|
-
# will free up ~200 bytes of unswappable kernel memory,
|
41
|
-
# so we call it first
|
42
|
-
epoll_ctl(Epoll::CTL_DEL, io, 0)
|
43
|
-
@fdmap.forget(io)
|
37
|
+
Thread.current[:yahns_queue] = self
|
44
38
|
end
|
45
39
|
|
46
40
|
# returns an array of infinitely running threads
|
@@ -64,13 +58,13 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
|
|
64
58
|
# expected to work, so we had to erase it from fdmap before hijack
|
65
59
|
when nil, :close
|
66
60
|
# this must be the ONLY place where we call IO#close on
|
67
|
-
# things that got inside the queue
|
61
|
+
# things that got inside the queue AND fdmap
|
68
62
|
@fdmap.sync_close(io)
|
69
63
|
else
|
70
64
|
raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
|
71
65
|
end
|
72
66
|
end
|
73
|
-
rescue => e
|
67
|
+
rescue StandardError, LoadError, SyntaxError => e
|
74
68
|
break if closed? # can still happen due to shutdown_timeout
|
75
69
|
Yahns::Log.exception(logger, 'queue loop', e)
|
76
70
|
end while true
|
data/lib/yahns/queue_kqueue.rb
CHANGED
@@ -36,15 +36,14 @@ class Yahns::Queue < SleepyPenguin::Kqueue::IO # :nodoc:
|
|
36
36
|
kevent(Kevent[io.fileno, flags, fflags, 0, 0, io])
|
37
37
|
end
|
38
38
|
|
39
|
+
def queue_mod(io, flags)
|
40
|
+
kevent(Kevent[io.fileno, flags, ADD_ONESHOT, 0, 0, io])
|
41
|
+
end
|
42
|
+
|
39
43
|
def thr_init
|
40
44
|
Thread.current[:yahns_rbuf] = ""
|
41
45
|
Thread.current[:yahns_fdmap] = @fdmap
|
42
|
-
|
43
|
-
|
44
|
-
def queue_del(io)
|
45
|
-
# do not bother with kevent EV_DELETE, it may be tricky to get right,
|
46
|
-
# we only did it in epoll since Eric knows the epoll internals well.
|
47
|
-
@fdmap.forget(io)
|
46
|
+
Thread.current[:yahns_queue] = self
|
48
47
|
end
|
49
48
|
|
50
49
|
# returns an array of infinitely running threads
|
@@ -67,13 +66,13 @@ class Yahns::Queue < SleepyPenguin::Kqueue::IO # :nodoc:
|
|
67
66
|
# expected to work, so we had to erase it from fdmap before hijack
|
68
67
|
when nil, :close
|
69
68
|
# this must be the ONLY place where we call IO#close on
|
70
|
-
# things that got inside the queue
|
69
|
+
# things that got inside the queue AND fdmap
|
71
70
|
@fdmap.sync_close(io)
|
72
71
|
else
|
73
72
|
raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
|
74
73
|
end
|
75
74
|
end
|
76
|
-
rescue => e
|
75
|
+
rescue StandardError, LoadError, SyntaxError => e
|
77
76
|
break if closed? # can still happen due to shutdown_timeout
|
78
77
|
Yahns::Log.exception(logger, 'queue loop', e)
|
79
78
|
end while true
|
data/lib/yahns/rackup_handler.rb
CHANGED
data/lib/yahns/socket_helper.rb
CHANGED
@@ -8,7 +8,7 @@ module Yahns::SocketHelper # :nodoc:
|
|
8
8
|
def so_reuseport
|
9
9
|
if defined?(Socket::SO_REUSEPORT)
|
10
10
|
Socket::SO_REUSEPORT
|
11
|
-
elsif RUBY_PLATFORM
|
11
|
+
elsif RUBY_PLATFORM.include?('linux')
|
12
12
|
15 # only tested on x86_64 and i686
|
13
13
|
else
|
14
14
|
nil
|
@@ -108,7 +108,7 @@ module Yahns::SocketHelper # :nodoc:
|
|
108
108
|
# returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
|
109
109
|
def tcp_name(sock)
|
110
110
|
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
|
111
|
-
|
111
|
+
addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
|
112
112
|
end
|
113
113
|
|
114
114
|
# Returns the configuration name of a socket as a string. sock may
|
data/lib/yahns/tee_input.rb
CHANGED
data/lib/yahns/tmpio.rb
CHANGED
@@ -7,14 +7,15 @@ require 'tmpdir'
|
|
7
7
|
# well with unlinked files. This one is much shorter, easier
|
8
8
|
# to understand, and slightly faster (no delegation).
|
9
9
|
class Yahns::TmpIO < File # :nodoc:
|
10
|
+
include Kgio::PipeMethods
|
10
11
|
|
11
12
|
# creates and returns a new File object. The File is unlinked
|
12
13
|
# immediately, switched to binary mode, and userspace output
|
13
14
|
# buffering is disabled
|
14
|
-
def self.new(
|
15
|
+
def self.new(dir)
|
15
16
|
retried = false
|
16
17
|
begin
|
17
|
-
fp = super("#{tmpdir}/#{rand}", RDWR|CREAT|EXCL|APPEND, 0600)
|
18
|
+
fp = super("#{dir || Dir.tmpdir}/#{rand}", RDWR|CREAT|EXCL|APPEND, 0600)
|
18
19
|
rescue Errno::EEXIST
|
19
20
|
retry
|
20
21
|
rescue Errno::EMFILE, Errno::ENFILE
|
@@ -29,4 +30,7 @@ class Yahns::TmpIO < File # :nodoc:
|
|
29
30
|
fp.sync = true
|
30
31
|
fp
|
31
32
|
end
|
33
|
+
|
34
|
+
# pretend we're Tempfile for Rack::TempfileReaper
|
35
|
+
alias close! close
|
32
36
|
end
|
data/lib/yahns/wbuf.rb
CHANGED
@@ -29,34 +29,46 @@ require_relative 'wbuf_common'
|
|
29
29
|
# to be a scalability issue.
|
30
30
|
class Yahns::Wbuf # :nodoc:
|
31
31
|
include Yahns::WbufCommon
|
32
|
+
attr_reader :busy
|
33
|
+
attr_reader :wbuf_persist
|
32
34
|
|
33
|
-
def initialize(body, persist, tmpdir)
|
35
|
+
def initialize(body, persist, tmpdir, busy)
|
34
36
|
@tmpio = nil
|
35
37
|
@tmpdir = tmpdir
|
36
38
|
@sf_offset = @sf_count = 0
|
37
39
|
@wbuf_persist = persist # whether or not we keep the connection alive
|
38
40
|
@body = body
|
39
|
-
@
|
41
|
+
@busy = busy # may be false
|
40
42
|
end
|
41
43
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
def wbuf_writev(buf)
|
45
|
+
@tmpio.kgio_writev(buf)
|
46
|
+
buf.inject(0) { |n, s| n += s.size }
|
47
|
+
end
|
48
|
+
|
49
|
+
def wbuf_write(c, buf)
|
50
|
+
# try to bypass the VFS layer and write directly to the socket
|
51
|
+
# if we're all caught up
|
52
|
+
case rv = String === buf ? c.kgio_trywrite(buf) : c.kgio_trywritev(buf)
|
53
|
+
when String, Array
|
46
54
|
buf = rv # retry in loop
|
47
55
|
when nil
|
48
56
|
return # yay! hopefully we don't have to buffer again
|
49
57
|
when :wait_writable, :wait_readable
|
50
|
-
@
|
51
|
-
end
|
58
|
+
@busy = rv
|
59
|
+
end until @busy
|
52
60
|
|
53
61
|
@tmpio ||= Yahns::TmpIO.new(@tmpdir)
|
54
|
-
@sf_count += @tmpio.write(buf)
|
55
|
-
|
62
|
+
@sf_count += String === buf ? @tmpio.write(buf) : wbuf_writev(buf)
|
63
|
+
|
64
|
+
# we spent some time copying to the FS, try to write to
|
65
|
+
# the socket again in case some space opened up...
|
66
|
+
case rv = c.trysendfile(@tmpio, @sf_offset, @sf_count)
|
56
67
|
when Integer
|
57
68
|
@sf_count -= rv
|
58
69
|
@sf_offset += rv
|
59
70
|
when :wait_writable, :wait_readable
|
71
|
+
@busy = rv
|
60
72
|
return rv
|
61
73
|
else
|
62
74
|
raise "BUG: #{rv.nil ? "EOF" : rv.inspect} on tmpio " \
|
@@ -65,15 +77,19 @@ class Yahns::Wbuf # :nodoc:
|
|
65
77
|
|
66
78
|
# we're all caught up, try to prevent dirty data from getting flushed
|
67
79
|
# to disk if we can help it.
|
68
|
-
|
80
|
+
wbuf_abort
|
69
81
|
@sf_offset = 0
|
70
|
-
@
|
82
|
+
@busy = false
|
71
83
|
nil
|
72
84
|
end
|
73
85
|
|
74
86
|
# called by last wbuf_flush
|
75
87
|
def wbuf_close(client)
|
76
|
-
|
88
|
+
wbuf_abort
|
77
89
|
wbuf_close_common(client)
|
78
90
|
end
|
91
|
+
|
92
|
+
def wbuf_abort
|
93
|
+
@tmpio = @tmpio.close if @tmpio
|
94
|
+
end
|
79
95
|
end
|