cool.io 0.9.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.
- data/.gitignore +25 -0
- data/CHANGES +199 -0
- data/LICENSE +20 -0
- data/README.markdown +4 -0
- data/Rakefile +98 -0
- data/VERSION +1 -0
- data/examples/echo_client.rb +38 -0
- data/examples/echo_server.rb +27 -0
- data/examples/google.rb +9 -0
- data/examples/httpclient.rb +38 -0
- data/ext/cool.io/.gitignore +5 -0
- data/ext/cool.io/cool.io.h +58 -0
- data/ext/cool.io/cool.io_ext.c +25 -0
- data/ext/cool.io/ev_wrap.h +8 -0
- data/ext/cool.io/extconf.rb +69 -0
- data/ext/cool.io/iowatcher.c +189 -0
- data/ext/cool.io/libev.c +8 -0
- data/ext/cool.io/loop.c +303 -0
- data/ext/cool.io/stat_watcher.c +191 -0
- data/ext/cool.io/timer_watcher.c +219 -0
- data/ext/cool.io/utils.c +122 -0
- data/ext/cool.io/watcher.c +264 -0
- data/ext/cool.io/watcher.h +71 -0
- data/ext/http11_client/.gitignore +5 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.c +300 -0
- data/ext/http11_client/http11_parser.c +403 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.rl +173 -0
- data/ext/libev/Changes +364 -0
- data/ext/libev/LICENSE +36 -0
- data/ext/libev/README +58 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +3867 -0
- data/ext/libev/ev.h +826 -0
- data/ext/libev/ev_epoll.c +234 -0
- data/ext/libev/ev_kqueue.c +198 -0
- data/ext/libev/ev_poll.c +148 -0
- data/ext/libev/ev_port.c +164 -0
- data/ext/libev/ev_select.c +307 -0
- data/ext/libev/ev_vars.h +197 -0
- data/ext/libev/ev_win32.c +153 -0
- data/ext/libev/ev_wrap.h +186 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/ext/libev/update_ev_wrap +19 -0
- data/lib/.gitignore +2 -0
- data/lib/cool.io.rb +30 -0
- data/lib/cool.io/async_watcher.rb +43 -0
- data/lib/cool.io/dns_resolver.rb +220 -0
- data/lib/cool.io/eventmachine.rb +234 -0
- data/lib/cool.io/http_client.rb +419 -0
- data/lib/cool.io/io.rb +174 -0
- data/lib/cool.io/iowatcher.rb +17 -0
- data/lib/cool.io/listener.rb +93 -0
- data/lib/cool.io/loop.rb +130 -0
- data/lib/cool.io/meta.rb +49 -0
- data/lib/cool.io/server.rb +74 -0
- data/lib/cool.io/socket.rb +224 -0
- data/lib/cool.io/timer_watcher.rb +17 -0
- data/lib/coolio.rb +2 -0
- data/lib/rev.rb +4 -0
- data/spec/async_watcher_spec.rb +57 -0
- data/spec/possible_tests/schedules_other_threads.rb +48 -0
- data/spec/possible_tests/test_on_resolve_failed.rb +9 -0
- data/spec/possible_tests/test_resolves.rb +27 -0
- data/spec/possible_tests/test_write_during_resolve.rb +27 -0
- data/spec/possible_tests/works_straight.rb +71 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/timer_watcher_spec.rb +55 -0
- data/spec/unix_listener_spec.rb +25 -0
- data/spec/unix_server_spec.rb +25 -0
- metadata +184 -0
@@ -0,0 +1,419 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# Includes portions originally Copyright (C)2005 Zed Shaw
|
4
|
+
# You can redistribute this under the terms of the Ruby license
|
5
|
+
# See file LICENSE for details
|
6
|
+
#++
|
7
|
+
|
8
|
+
require 'http11_client'
|
9
|
+
|
10
|
+
module Coolio
|
11
|
+
# A simple hash is returned for each request made by HttpClient with
|
12
|
+
# the headers that were given by the server for that request.
|
13
|
+
class HttpResponseHeader < Hash
|
14
|
+
# The reason returned in the http response ("OK","File not found",etc.)
|
15
|
+
attr_accessor :http_reason
|
16
|
+
|
17
|
+
# The HTTP version returned.
|
18
|
+
attr_accessor :http_version
|
19
|
+
|
20
|
+
# The status code (as a string!)
|
21
|
+
attr_accessor :http_status
|
22
|
+
|
23
|
+
# HTTP response status as an integer
|
24
|
+
def status
|
25
|
+
Integer(http_status) rescue nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Length of content as an integer, or nil if chunked/unspecified
|
29
|
+
def content_length
|
30
|
+
Integer(self[HttpClient::CONTENT_LENGTH]) rescue nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Is the transfer encoding chunked?
|
34
|
+
def chunked_encoding?
|
35
|
+
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class HttpChunkHeader < Hash
|
40
|
+
# When parsing chunked encodings this is set
|
41
|
+
attr_accessor :http_chunk_size
|
42
|
+
|
43
|
+
# Size of the chunk as an integer
|
44
|
+
def chunk_size
|
45
|
+
return @chunk_size unless @chunk_size.nil?
|
46
|
+
@chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Methods for building HTTP requests
|
51
|
+
module HttpEncoding
|
52
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
53
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
54
|
+
|
55
|
+
# Escapes a URI.
|
56
|
+
def escape(s)
|
57
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
58
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
59
|
+
}.tr(' ', '+')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Unescapes a URI escaped string.
|
63
|
+
def unescape(s)
|
64
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
65
|
+
[$1.delete('%')].pack('H*')
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Map all header keys to a downcased string version
|
70
|
+
def munge_header_keys(head)
|
71
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
72
|
+
end
|
73
|
+
|
74
|
+
# HTTP is kind of retarded that you have to specify
|
75
|
+
# a Host header, but if you include port 80 then further
|
76
|
+
# redirects will tack on the :80 which is annoying.
|
77
|
+
def encode_host
|
78
|
+
remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
|
79
|
+
end
|
80
|
+
|
81
|
+
def encode_request(method, path, query)
|
82
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
|
83
|
+
end
|
84
|
+
|
85
|
+
def encode_query(path, query)
|
86
|
+
return path unless query
|
87
|
+
path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
|
88
|
+
end
|
89
|
+
|
90
|
+
# URL encodes a single k=v parameter.
|
91
|
+
def encode_param(k, v)
|
92
|
+
escape(k) + "=" + escape(v)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Encode a field in an HTTP header
|
96
|
+
def encode_field(k, v)
|
97
|
+
FIELD_ENCODING % [k, v]
|
98
|
+
end
|
99
|
+
|
100
|
+
def encode_headers(head)
|
101
|
+
head.inject('') do |result, (key, value)|
|
102
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
103
|
+
key = key.split('-').map { |k| k.capitalize }.join('-')
|
104
|
+
result << encode_field(key, value)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def encode_cookies(cookies)
|
109
|
+
cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# HTTP client class implemented as a subclass of Coolio::TCPSocket. Encodes
|
114
|
+
# requests and allows streaming consumption of the response. Response is
|
115
|
+
# parsed with a Ragel-generated whitelist parser which supports chunked
|
116
|
+
# HTTP encoding.
|
117
|
+
#
|
118
|
+
# == Example
|
119
|
+
#
|
120
|
+
# loop = Coolio::Loop.default
|
121
|
+
# client = Coolio::HttpClient.connect("www.google.com").attach
|
122
|
+
# client.get('/search', query: {q: 'foobar'})
|
123
|
+
# loop.run
|
124
|
+
#
|
125
|
+
class HttpClient < TCPSocket
|
126
|
+
include HttpEncoding
|
127
|
+
|
128
|
+
ALLOWED_METHODS=[:put, :get, :post, :delete, :head]
|
129
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
130
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
131
|
+
SET_COOKIE="SET_COOKIE"
|
132
|
+
LOCATION="LOCATION"
|
133
|
+
HOST="HOST"
|
134
|
+
CRLF="\r\n"
|
135
|
+
|
136
|
+
# Connect to the given server, with port 80 as the default
|
137
|
+
def self.connect(addr, port = 80, *args)
|
138
|
+
super
|
139
|
+
end
|
140
|
+
|
141
|
+
def initialize(socket)
|
142
|
+
super
|
143
|
+
|
144
|
+
@parser = HttpClientParser.new
|
145
|
+
@parser_nbytes = 0
|
146
|
+
|
147
|
+
@state = :response_header
|
148
|
+
@data = ::IO::Buffer.new
|
149
|
+
|
150
|
+
@response_header = HttpResponseHeader.new
|
151
|
+
@chunk_header = HttpChunkHeader.new
|
152
|
+
end
|
153
|
+
|
154
|
+
# Send an HTTP request and consume the response.
|
155
|
+
# Supports the following options:
|
156
|
+
#
|
157
|
+
# head: {Key: Value}
|
158
|
+
# Specify an HTTP header, e.g. {'Connection': 'close'}
|
159
|
+
#
|
160
|
+
# query: {Key: Value}
|
161
|
+
# Specify query string parameters (auto-escaped)
|
162
|
+
#
|
163
|
+
# cookies: {Key: Value}
|
164
|
+
# Specify hash of cookies (auto-escaped)
|
165
|
+
#
|
166
|
+
# body: String
|
167
|
+
# Specify the request body (you must encode it for now)
|
168
|
+
#
|
169
|
+
def request(method, path, options = {})
|
170
|
+
raise ArgumentError, "invalid request path" unless /^\// === path
|
171
|
+
raise RuntimeError, "request already sent" if @requested
|
172
|
+
|
173
|
+
@method, @path, @options = method, path, options
|
174
|
+
@requested = true
|
175
|
+
|
176
|
+
return unless @connected
|
177
|
+
send_request
|
178
|
+
end
|
179
|
+
|
180
|
+
# Enable the HttpClient if it has been disabled
|
181
|
+
def enable
|
182
|
+
super
|
183
|
+
dispatch unless @data.empty?
|
184
|
+
end
|
185
|
+
|
186
|
+
# Called when response header has been received
|
187
|
+
def on_response_header(response_header)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Called when part of the body has been read
|
191
|
+
def on_body_data(data)
|
192
|
+
STDOUT.write data
|
193
|
+
STDOUT.flush
|
194
|
+
end
|
195
|
+
|
196
|
+
# Called when the request has completed
|
197
|
+
def on_request_complete
|
198
|
+
close
|
199
|
+
end
|
200
|
+
|
201
|
+
# Called when an error occurs dispatching the request
|
202
|
+
def on_error(reason)
|
203
|
+
close
|
204
|
+
raise RuntimeError, reason
|
205
|
+
end
|
206
|
+
|
207
|
+
#########
|
208
|
+
protected
|
209
|
+
#########
|
210
|
+
|
211
|
+
#
|
212
|
+
# Coolio callbacks
|
213
|
+
#
|
214
|
+
|
215
|
+
def on_connect
|
216
|
+
@connected = true
|
217
|
+
send_request if @method and @path
|
218
|
+
end
|
219
|
+
|
220
|
+
def on_read(data)
|
221
|
+
@data << data
|
222
|
+
dispatch
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Request sending
|
227
|
+
#
|
228
|
+
|
229
|
+
def send_request
|
230
|
+
send_request_header
|
231
|
+
send_request_body
|
232
|
+
end
|
233
|
+
|
234
|
+
def send_request_header
|
235
|
+
query = @options[:query]
|
236
|
+
head = @options[:head] ? munge_header_keys(@options[:head]) : {}
|
237
|
+
cookies = @options[:cookies]
|
238
|
+
body = @options[:body]
|
239
|
+
|
240
|
+
# Set the Host header if it hasn't been specified already
|
241
|
+
head['host'] ||= encode_host
|
242
|
+
|
243
|
+
# Set the Content-Length if it hasn't been specified already and a body was given
|
244
|
+
head['content-length'] ||= body ? body.length : 0
|
245
|
+
|
246
|
+
# Set the User-Agent if it hasn't been specified
|
247
|
+
head['user-agent'] ||= "Coolio #{Coolio::VERSION}"
|
248
|
+
|
249
|
+
# Default to Connection: close
|
250
|
+
head['connection'] ||= 'close'
|
251
|
+
|
252
|
+
# Build the request
|
253
|
+
request_header = encode_request(@method, @path, query)
|
254
|
+
request_header << encode_headers(head)
|
255
|
+
request_header << encode_cookies(cookies) if cookies
|
256
|
+
request_header << CRLF
|
257
|
+
|
258
|
+
write request_header
|
259
|
+
end
|
260
|
+
|
261
|
+
def send_request_body
|
262
|
+
write @options[:body] if @options[:body]
|
263
|
+
end
|
264
|
+
|
265
|
+
#
|
266
|
+
# Response processing
|
267
|
+
#
|
268
|
+
|
269
|
+
def dispatch
|
270
|
+
while enabled? and case @state
|
271
|
+
when :response_header
|
272
|
+
parse_response_header
|
273
|
+
when :chunk_header
|
274
|
+
parse_chunk_header
|
275
|
+
when :chunk_body
|
276
|
+
process_chunk_body
|
277
|
+
when :chunk_footer
|
278
|
+
process_chunk_footer
|
279
|
+
when :response_footer
|
280
|
+
process_response_footer
|
281
|
+
when :body
|
282
|
+
process_body
|
283
|
+
when :finished, :invalid
|
284
|
+
break
|
285
|
+
else raise RuntimeError, "invalid state: #{@state}"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def parse_header(header)
|
291
|
+
return false if @data.empty?
|
292
|
+
|
293
|
+
begin
|
294
|
+
@parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
|
295
|
+
rescue Coolio::HttpClientParserError
|
296
|
+
on_error "invalid HTTP format, parsing fails"
|
297
|
+
@state = :invalid
|
298
|
+
end
|
299
|
+
|
300
|
+
return false unless @parser.finished?
|
301
|
+
|
302
|
+
# Clear parsed data from the buffer
|
303
|
+
@data.read(@parser_nbytes)
|
304
|
+
@parser.reset
|
305
|
+
@parser_nbytes = 0
|
306
|
+
|
307
|
+
true
|
308
|
+
end
|
309
|
+
|
310
|
+
def parse_response_header
|
311
|
+
return false unless parse_header(@response_header)
|
312
|
+
|
313
|
+
unless @response_header.http_status and @response_header.http_reason
|
314
|
+
on_error "no HTTP response"
|
315
|
+
@state = :invalid
|
316
|
+
return false
|
317
|
+
end
|
318
|
+
|
319
|
+
on_response_header(@response_header)
|
320
|
+
|
321
|
+
if @response_header.chunked_encoding?
|
322
|
+
@state = :chunk_header
|
323
|
+
else
|
324
|
+
@state = :body
|
325
|
+
@bytes_remaining = @response_header.content_length
|
326
|
+
end
|
327
|
+
|
328
|
+
true
|
329
|
+
end
|
330
|
+
|
331
|
+
def parse_chunk_header
|
332
|
+
return false unless parse_header(@chunk_header)
|
333
|
+
|
334
|
+
@bytes_remaining = @chunk_header.chunk_size
|
335
|
+
@chunk_header = HttpChunkHeader.new
|
336
|
+
|
337
|
+
@state = @bytes_remaining > 0 ? :chunk_body : :response_footer
|
338
|
+
true
|
339
|
+
end
|
340
|
+
|
341
|
+
def process_chunk_body
|
342
|
+
if @data.size < @bytes_remaining
|
343
|
+
@bytes_remaining -= @data.size
|
344
|
+
on_body_data @data.read
|
345
|
+
return false
|
346
|
+
end
|
347
|
+
|
348
|
+
on_body_data @data.read(@bytes_remaining)
|
349
|
+
@bytes_remaining = 0
|
350
|
+
|
351
|
+
@state = :chunk_footer
|
352
|
+
true
|
353
|
+
end
|
354
|
+
|
355
|
+
def process_chunk_footer
|
356
|
+
return false if @data.size < 2
|
357
|
+
|
358
|
+
if @data.read(2) == CRLF
|
359
|
+
@state = :chunk_header
|
360
|
+
else
|
361
|
+
on_error "non-CRLF chunk footer"
|
362
|
+
@state = :invalid
|
363
|
+
end
|
364
|
+
|
365
|
+
true
|
366
|
+
end
|
367
|
+
|
368
|
+
def process_response_footer
|
369
|
+
return false if @data.size < 2
|
370
|
+
|
371
|
+
if @data.read(2) == CRLF
|
372
|
+
if @data.empty?
|
373
|
+
on_request_complete
|
374
|
+
@state = :finished
|
375
|
+
else
|
376
|
+
on_error "garbage at end of chunked response"
|
377
|
+
@state = :invalid
|
378
|
+
end
|
379
|
+
else
|
380
|
+
on_error "non-CRLF response footer"
|
381
|
+
@state = :invalid
|
382
|
+
end
|
383
|
+
|
384
|
+
false
|
385
|
+
end
|
386
|
+
|
387
|
+
def process_body
|
388
|
+
if @bytes_remaining.nil?
|
389
|
+
on_body_data @data.read
|
390
|
+
return false
|
391
|
+
end
|
392
|
+
|
393
|
+
if @bytes_remaining.zero?
|
394
|
+
on_request_complete
|
395
|
+
@state = :finished
|
396
|
+
return false
|
397
|
+
end
|
398
|
+
|
399
|
+
if @data.size < @bytes_remaining
|
400
|
+
@bytes_remaining -= @data.size
|
401
|
+
on_body_data @data.read
|
402
|
+
return false
|
403
|
+
end
|
404
|
+
|
405
|
+
on_body_data @data.read(@bytes_remaining)
|
406
|
+
@bytes_remaining = 0
|
407
|
+
|
408
|
+
if @data.empty?
|
409
|
+
on_request_complete
|
410
|
+
@state = :finished
|
411
|
+
else
|
412
|
+
on_error "garbage at end of body"
|
413
|
+
@state = :invalid
|
414
|
+
end
|
415
|
+
|
416
|
+
false
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
data/lib/cool.io/io.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007-10 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Coolio
|
8
|
+
# A buffered I/O class witch fits into the Coolio Watcher framework.
|
9
|
+
# It provides both an observer which reads data as it's received
|
10
|
+
# from the wire and a buffered write watcher which stores data and writes
|
11
|
+
# it out each time the socket becomes writable.
|
12
|
+
#
|
13
|
+
# This class is primarily meant as a base class for other streams
|
14
|
+
# which need non-blocking writing, and is used to implement Coolio's
|
15
|
+
# Socket class and its associated subclasses.
|
16
|
+
class IO
|
17
|
+
extend Meta
|
18
|
+
|
19
|
+
# Maximum number of bytes to consume at once
|
20
|
+
INPUT_SIZE = 16384
|
21
|
+
|
22
|
+
def initialize(io)
|
23
|
+
@_io = io
|
24
|
+
@_write_buffer ||= ::IO::Buffer.new
|
25
|
+
@_read_watcher = Watcher.new(io, self, :r)
|
26
|
+
@_write_watcher = Watcher.new(io, self, :w)
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Watcher methods, delegated to @_read_watcher
|
31
|
+
#
|
32
|
+
|
33
|
+
# Attach to the event loop
|
34
|
+
def attach(loop); @_read_watcher.attach loop; schedule_write if !@_write_buffer.empty?; self; end
|
35
|
+
|
36
|
+
# Detach from the event loop
|
37
|
+
def detach; @_read_watcher.detach; self; end # TODO should these detect write buffers, as well?
|
38
|
+
|
39
|
+
# Enable the watcher
|
40
|
+
def enable; @_read_watcher.enable; self; end
|
41
|
+
|
42
|
+
# Disable the watcher
|
43
|
+
def disable; @_read_watcher.disable; self; end
|
44
|
+
|
45
|
+
# Is the watcher attached?
|
46
|
+
def attached?; @_read_watcher.attached?; end
|
47
|
+
|
48
|
+
# Is the watcher enabled?
|
49
|
+
def enabled?; @_read_watcher.enabled?; end
|
50
|
+
|
51
|
+
# Obtain the event loop associated with this object
|
52
|
+
def evloop; @_read_watcher.evloop; end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Callbacks for asynchronous events
|
56
|
+
#
|
57
|
+
|
58
|
+
# Called whenever the IO object receives data
|
59
|
+
def on_read(data); end
|
60
|
+
event_callback :on_read
|
61
|
+
|
62
|
+
# Called whenever a write completes and the output buffer is empty
|
63
|
+
def on_write_complete; end
|
64
|
+
event_callback :on_write_complete
|
65
|
+
|
66
|
+
# Called whenever the IO object hits EOF
|
67
|
+
def on_close; end
|
68
|
+
event_callback :on_close
|
69
|
+
|
70
|
+
#
|
71
|
+
# Write interface
|
72
|
+
#
|
73
|
+
|
74
|
+
# Write data in a buffered, non-blocking manner
|
75
|
+
def write(data)
|
76
|
+
@_write_buffer << data
|
77
|
+
schedule_write
|
78
|
+
data.size
|
79
|
+
end
|
80
|
+
|
81
|
+
# Number of bytes are currently in the output buffer
|
82
|
+
def output_buffer_size
|
83
|
+
@_write_buffer.size
|
84
|
+
end
|
85
|
+
|
86
|
+
# Close the IO stream
|
87
|
+
def close
|
88
|
+
detach if attached?
|
89
|
+
detach_write_watcher
|
90
|
+
@_io.close unless @_io.closed?
|
91
|
+
|
92
|
+
on_close
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
# Is the IO object closed?
|
97
|
+
def closed?
|
98
|
+
@_io.nil? or @_io.closed?
|
99
|
+
end
|
100
|
+
|
101
|
+
#########
|
102
|
+
protected
|
103
|
+
#########
|
104
|
+
|
105
|
+
# Read from the input buffer and dispatch to on_read
|
106
|
+
def on_readable
|
107
|
+
begin
|
108
|
+
on_read @_io.read_nonblock(INPUT_SIZE)
|
109
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
110
|
+
return
|
111
|
+
|
112
|
+
# SystemCallError catches Errno::ECONNRESET amongst others.
|
113
|
+
rescue SystemCallError, EOFError, IOError, SocketError
|
114
|
+
close
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Write the contents of the output buffer
|
119
|
+
def on_writable
|
120
|
+
begin
|
121
|
+
@_write_buffer.write_to(@_io)
|
122
|
+
rescue Errno::EINTR
|
123
|
+
return
|
124
|
+
|
125
|
+
# SystemCallError catches Errno::EPIPE & Errno::ECONNRESET amongst others.
|
126
|
+
rescue SystemCallError, IOError, SocketError
|
127
|
+
return close
|
128
|
+
end
|
129
|
+
|
130
|
+
if @_write_buffer.empty?
|
131
|
+
disable_write_watcher
|
132
|
+
on_write_complete
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Schedule a write to be performed when the IO object becomes writable
|
137
|
+
def schedule_write
|
138
|
+
return unless @_io # this would mean 'we are still pre DNS here'
|
139
|
+
return unless attached? # this would mean 'currently unattached' -- ie still pre DNS, or just plain not attached, which is ok
|
140
|
+
begin
|
141
|
+
enable_write_watcher
|
142
|
+
rescue IOError
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def enable_write_watcher
|
147
|
+
if @_write_watcher.attached?
|
148
|
+
@_write_watcher.enable unless @_write_watcher.enabled?
|
149
|
+
else
|
150
|
+
@_write_watcher.attach(evloop)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def disable_write_watcher
|
155
|
+
@_write_watcher.disable if @_write_watcher and @_write_watcher.enabled?
|
156
|
+
end
|
157
|
+
|
158
|
+
def detach_write_watcher
|
159
|
+
@_write_watcher.detach if @_write_watcher and @_write_watcher.attached?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Internal class implementing watchers used by Coolio::IO
|
163
|
+
class Watcher < IOWatcher
|
164
|
+
def initialize(ruby_io, coolio_io, flags)
|
165
|
+
@coolio_io = coolio_io
|
166
|
+
super(ruby_io, flags)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Configure IOWatcher event callbacks to call the method passed to #initialize
|
170
|
+
def on_readable; @coolio_io.__send__(:on_readable); end
|
171
|
+
def on_writable; @coolio_io.__send__(:on_writable); end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|