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.
Files changed (73) hide show
  1. data/.gitignore +25 -0
  2. data/CHANGES +199 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +4 -0
  5. data/Rakefile +98 -0
  6. data/VERSION +1 -0
  7. data/examples/echo_client.rb +38 -0
  8. data/examples/echo_server.rb +27 -0
  9. data/examples/google.rb +9 -0
  10. data/examples/httpclient.rb +38 -0
  11. data/ext/cool.io/.gitignore +5 -0
  12. data/ext/cool.io/cool.io.h +58 -0
  13. data/ext/cool.io/cool.io_ext.c +25 -0
  14. data/ext/cool.io/ev_wrap.h +8 -0
  15. data/ext/cool.io/extconf.rb +69 -0
  16. data/ext/cool.io/iowatcher.c +189 -0
  17. data/ext/cool.io/libev.c +8 -0
  18. data/ext/cool.io/loop.c +303 -0
  19. data/ext/cool.io/stat_watcher.c +191 -0
  20. data/ext/cool.io/timer_watcher.c +219 -0
  21. data/ext/cool.io/utils.c +122 -0
  22. data/ext/cool.io/watcher.c +264 -0
  23. data/ext/cool.io/watcher.h +71 -0
  24. data/ext/http11_client/.gitignore +5 -0
  25. data/ext/http11_client/ext_help.h +14 -0
  26. data/ext/http11_client/extconf.rb +6 -0
  27. data/ext/http11_client/http11_client.c +300 -0
  28. data/ext/http11_client/http11_parser.c +403 -0
  29. data/ext/http11_client/http11_parser.h +48 -0
  30. data/ext/http11_client/http11_parser.rl +173 -0
  31. data/ext/libev/Changes +364 -0
  32. data/ext/libev/LICENSE +36 -0
  33. data/ext/libev/README +58 -0
  34. data/ext/libev/README.embed +3 -0
  35. data/ext/libev/ev.c +3867 -0
  36. data/ext/libev/ev.h +826 -0
  37. data/ext/libev/ev_epoll.c +234 -0
  38. data/ext/libev/ev_kqueue.c +198 -0
  39. data/ext/libev/ev_poll.c +148 -0
  40. data/ext/libev/ev_port.c +164 -0
  41. data/ext/libev/ev_select.c +307 -0
  42. data/ext/libev/ev_vars.h +197 -0
  43. data/ext/libev/ev_win32.c +153 -0
  44. data/ext/libev/ev_wrap.h +186 -0
  45. data/ext/libev/test_libev_win32.c +123 -0
  46. data/ext/libev/update_ev_wrap +19 -0
  47. data/lib/.gitignore +2 -0
  48. data/lib/cool.io.rb +30 -0
  49. data/lib/cool.io/async_watcher.rb +43 -0
  50. data/lib/cool.io/dns_resolver.rb +220 -0
  51. data/lib/cool.io/eventmachine.rb +234 -0
  52. data/lib/cool.io/http_client.rb +419 -0
  53. data/lib/cool.io/io.rb +174 -0
  54. data/lib/cool.io/iowatcher.rb +17 -0
  55. data/lib/cool.io/listener.rb +93 -0
  56. data/lib/cool.io/loop.rb +130 -0
  57. data/lib/cool.io/meta.rb +49 -0
  58. data/lib/cool.io/server.rb +74 -0
  59. data/lib/cool.io/socket.rb +224 -0
  60. data/lib/cool.io/timer_watcher.rb +17 -0
  61. data/lib/coolio.rb +2 -0
  62. data/lib/rev.rb +4 -0
  63. data/spec/async_watcher_spec.rb +57 -0
  64. data/spec/possible_tests/schedules_other_threads.rb +48 -0
  65. data/spec/possible_tests/test_on_resolve_failed.rb +9 -0
  66. data/spec/possible_tests/test_resolves.rb +27 -0
  67. data/spec/possible_tests/test_write_during_resolve.rb +27 -0
  68. data/spec/possible_tests/works_straight.rb +71 -0
  69. data/spec/spec_helper.rb +5 -0
  70. data/spec/timer_watcher_spec.rb +55 -0
  71. data/spec/unix_listener_spec.rb +25 -0
  72. data/spec/unix_server_spec.rb +25 -0
  73. 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