rev 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +58 -0
- data/README +107 -0
- data/ext/libev/ev.c +2440 -0
- data/ext/libev/ev.h +551 -0
- data/ext/libev/ev_epoll.c +174 -0
- data/ext/libev/ev_kqueue.c +186 -0
- data/ext/libev/ev_poll.c +127 -0
- data/ext/libev/ev_port.c +155 -0
- data/ext/libev/ev_select.c +236 -0
- data/ext/libev/ev_vars.h +108 -0
- data/ext/libev/ev_win32.c +117 -0
- data/ext/libev/ev_wrap.h +132 -0
- data/ext/rev/extconf.rb +36 -0
- data/ext/rev/rev.h +44 -0
- data/ext/rev/rev_ext.c +29 -0
- data/ext/rev/rev_io_watcher.c +157 -0
- data/ext/rev/rev_loop.c +254 -0
- data/ext/rev/rev_timer_watcher.c +153 -0
- data/ext/rev/rev_watcher.c +222 -0
- data/ext/rev/rev_watcher.h +79 -0
- data/lib/rev.rb +21 -0
- data/lib/rev/buffered_io.rb +123 -0
- data/lib/rev/dns_resolver.rb +178 -0
- data/lib/rev/io_watcher.rb +18 -0
- data/lib/rev/listener.rb +50 -0
- data/lib/rev/loop.rb +101 -0
- data/lib/rev/server.rb +53 -0
- data/lib/rev/socket.rb +186 -0
- data/lib/rev/timer_watcher.rb +18 -0
- data/lib/rev/watcher.rb +49 -0
- data/spec/rev_spec.rb +26 -0
- metadata +93 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
/*
|
2
|
+
* Copyright (C) 2007 Tony Arcieri
|
3
|
+
* You may redistribute this under the terms of the Ruby license.
|
4
|
+
* See LICENSE for details
|
5
|
+
*/
|
6
|
+
|
7
|
+
#ifndef REV_WATCHER_H
|
8
|
+
#define REV_WATCHER_H
|
9
|
+
|
10
|
+
#define Watcher_Attach(watcher_type, detach_func, watcher, loop) \
|
11
|
+
struct Rev_Watcher *watcher_data; \
|
12
|
+
struct Rev_Loop *loop_data; \
|
13
|
+
\
|
14
|
+
if(!rb_obj_is_kind_of(loop, cRev_Loop)) \
|
15
|
+
rb_raise(rb_eArgError, "expected loop to be an instance of Rev::Loop"); \
|
16
|
+
\
|
17
|
+
Data_Get_Struct(watcher, struct Rev_Watcher, watcher_data); \
|
18
|
+
Data_Get_Struct(loop, struct Rev_Loop, loop_data); \
|
19
|
+
\
|
20
|
+
if(watcher_data->loop != Qnil) \
|
21
|
+
detach_func(watcher); \
|
22
|
+
\
|
23
|
+
watcher_data->loop = loop; \
|
24
|
+
ev_##watcher_type##_start(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \
|
25
|
+
watcher_data->enabled = 1; \
|
26
|
+
rb_call_super(1, &loop)
|
27
|
+
|
28
|
+
#define Watcher_Detach(watcher_type, watcher) \
|
29
|
+
struct Rev_Watcher *watcher_data; \
|
30
|
+
struct Rev_Loop *loop_data; \
|
31
|
+
\
|
32
|
+
Data_Get_Struct(watcher, struct Rev_Watcher, watcher_data); \
|
33
|
+
\
|
34
|
+
if(watcher_data->loop == Qnil) \
|
35
|
+
rb_raise(rb_eRuntimeError, "not attached to a loop"); \
|
36
|
+
\
|
37
|
+
Data_Get_Struct(watcher_data->loop, struct Rev_Loop, loop_data); \
|
38
|
+
\
|
39
|
+
ev_##watcher_type##_stop(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \
|
40
|
+
watcher_data->enabled = 0; \
|
41
|
+
rb_call_super(0, 0)
|
42
|
+
|
43
|
+
#define Watcher_Enable(watcher_type, watcher) \
|
44
|
+
struct Rev_Watcher *watcher_data; \
|
45
|
+
struct Rev_Loop *loop_data; \
|
46
|
+
\
|
47
|
+
Data_Get_Struct(watcher, struct Rev_Watcher, watcher_data); \
|
48
|
+
\
|
49
|
+
if(watcher_data->enabled) \
|
50
|
+
rb_raise(rb_eRuntimeError, "already enabled"); \
|
51
|
+
\
|
52
|
+
if(watcher_data->loop == Qnil) \
|
53
|
+
rb_raise(rb_eRuntimeError, "not attached to a loop"); \
|
54
|
+
\
|
55
|
+
Data_Get_Struct(watcher_data->loop, struct Rev_Loop, loop_data); \
|
56
|
+
\
|
57
|
+
ev_##watcher_type##_start(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \
|
58
|
+
watcher_data->enabled = 1; \
|
59
|
+
rb_call_super(0, 0)
|
60
|
+
|
61
|
+
#define Watcher_Disable(watcher_type, watcher) \
|
62
|
+
struct Rev_Watcher *watcher_data; \
|
63
|
+
struct Rev_Loop *loop_data; \
|
64
|
+
\
|
65
|
+
Data_Get_Struct(watcher, struct Rev_Watcher, watcher_data); \
|
66
|
+
\
|
67
|
+
if(watcher_data->loop == Qnil) \
|
68
|
+
rb_raise(rb_eRuntimeError, "not attached to a loop"); \
|
69
|
+
\
|
70
|
+
if(!watcher_data->enabled) \
|
71
|
+
rb_raise(rb_eRuntimeError, "already disabled"); \
|
72
|
+
\
|
73
|
+
Data_Get_Struct(watcher_data->loop, struct Rev_Loop, loop_data); \
|
74
|
+
\
|
75
|
+
ev_##watcher_type##_stop(loop_data->ev_loop, &watcher_data->event_types.ev_##watcher_type); \
|
76
|
+
watcher_data->enabled = 0; \
|
77
|
+
rb_call_super(0, 0)
|
78
|
+
|
79
|
+
#endif
|
data/lib/rev.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/rev_ext'
|
8
|
+
require File.dirname(__FILE__) + '/rev/loop'
|
9
|
+
require File.dirname(__FILE__) + '/rev/watcher'
|
10
|
+
require File.dirname(__FILE__) + '/rev/io_watcher'
|
11
|
+
require File.dirname(__FILE__) + '/rev/timer_watcher'
|
12
|
+
require File.dirname(__FILE__) + '/rev/listener'
|
13
|
+
require File.dirname(__FILE__) + '/rev/buffered_io'
|
14
|
+
require File.dirname(__FILE__) + '/rev/dns_resolver'
|
15
|
+
require File.dirname(__FILE__) + '/rev/socket'
|
16
|
+
require File.dirname(__FILE__) + '/rev/server'
|
17
|
+
|
18
|
+
module Rev
|
19
|
+
Rev::VERSION = '0.1.0' unless defined? Rev::VERSION
|
20
|
+
def self.version() VERSION end
|
21
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../rev'
|
8
|
+
|
9
|
+
module Rev
|
10
|
+
class BufferedIO < IOWatcher
|
11
|
+
def initialize(io)
|
12
|
+
# Output buffer
|
13
|
+
@write_buffer = ''
|
14
|
+
|
15
|
+
# Coerce the argument into an IO object if possible
|
16
|
+
@io = IO.try_convert(io)
|
17
|
+
super(@io)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Callbacks for asynchronous events
|
22
|
+
#
|
23
|
+
|
24
|
+
# Called whenever the IO object receives data
|
25
|
+
def on_read(data); end
|
26
|
+
event_callback :on_read
|
27
|
+
|
28
|
+
# Called whenever a write completes and the output buffer is empty
|
29
|
+
def on_write_complete; end
|
30
|
+
event_callback :on_write_complete
|
31
|
+
|
32
|
+
# Called whenever the IO object hits EOF
|
33
|
+
def on_close; end
|
34
|
+
event_callback :on_close
|
35
|
+
|
36
|
+
#
|
37
|
+
# Write interface
|
38
|
+
#
|
39
|
+
|
40
|
+
# Write data in a buffered, non-blocking manner
|
41
|
+
def write(data)
|
42
|
+
# Attempt a zero copy write
|
43
|
+
if @write_buffer.empty?
|
44
|
+
written = @io.write_nonblock data
|
45
|
+
|
46
|
+
# If we lucked out and wrote out the whole buffer, return
|
47
|
+
if written == data.size
|
48
|
+
on_write_complete
|
49
|
+
return data.size
|
50
|
+
end
|
51
|
+
|
52
|
+
# Otherwise append the remaining data to the buffer
|
53
|
+
@write_buffer << data[written..data.size]
|
54
|
+
else
|
55
|
+
@write_buffer << data
|
56
|
+
end
|
57
|
+
|
58
|
+
schedule_write
|
59
|
+
data.size
|
60
|
+
end
|
61
|
+
|
62
|
+
# Number of bytes are currently in the output buffer
|
63
|
+
def output_buffer_size
|
64
|
+
@write_buffer.size
|
65
|
+
end
|
66
|
+
|
67
|
+
# Attempt to write the contents of the output buffer
|
68
|
+
def write_output_buffer
|
69
|
+
return if @write_buffer.empty?
|
70
|
+
|
71
|
+
written = @io.write_nonblock @write_buffer
|
72
|
+
@write_buffer.slice!(written, @write_buffer.size)
|
73
|
+
|
74
|
+
return unless @write_buffer.empty?
|
75
|
+
|
76
|
+
@writer.disable if @writer and @writer.enabled?
|
77
|
+
on_write_complete
|
78
|
+
end
|
79
|
+
|
80
|
+
# Close the BufferedIO stream
|
81
|
+
def close
|
82
|
+
detach if attached?
|
83
|
+
@writer.detach if @writer and @writer.attached?
|
84
|
+
@io.close
|
85
|
+
|
86
|
+
on_close
|
87
|
+
end
|
88
|
+
|
89
|
+
#########
|
90
|
+
protected
|
91
|
+
#########
|
92
|
+
|
93
|
+
# Inherited callback from IOWatcher
|
94
|
+
def on_readable
|
95
|
+
begin
|
96
|
+
on_read @io.read_nonblock(4096)
|
97
|
+
rescue EOFError
|
98
|
+
close
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def schedule_write
|
103
|
+
return if @writer and @writer.enabled?
|
104
|
+
if @writer
|
105
|
+
@writer.enable
|
106
|
+
else
|
107
|
+
@writer = Writer.new(@io, self)
|
108
|
+
@writer.attach(evloop)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Writer < IOWatcher
|
113
|
+
def initialize(io, buffered_io)
|
114
|
+
@buffered_io = buffered_io
|
115
|
+
super(io, :w)
|
116
|
+
end
|
117
|
+
|
118
|
+
def on_writable
|
119
|
+
@buffered_io.write_output_buffer
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../rev'
|
8
|
+
|
9
|
+
#--
|
10
|
+
# Gimpy hacka asynchronous DNS resolver
|
11
|
+
#
|
12
|
+
# Word to the wise: I don't know what I'm doing here. This was cobbled together as
|
13
|
+
# best I could with extremely limited knowledge of the DNS format. There's obviously
|
14
|
+
# a ton of stuff it doesn't support (like IPv6 and TCP).
|
15
|
+
#
|
16
|
+
# If you do know what you're doing with DNS, feel free to improve this!
|
17
|
+
#++
|
18
|
+
|
19
|
+
module Rev
|
20
|
+
class DNSResolver < IOWatcher
|
21
|
+
RESOLV_CONF = '/etc/resolv.conf'
|
22
|
+
HOSTS = '/etc/hosts'
|
23
|
+
DNS_PORT = 53
|
24
|
+
DATAGRAM_SIZE = 512
|
25
|
+
TIMEOUT = 3 # Retry timeout for each datagram sent
|
26
|
+
RETRIES = 4 # Number of retries to attempt
|
27
|
+
|
28
|
+
def self.hosts(host)
|
29
|
+
hosts = {}
|
30
|
+
File.open(HOSTS).each_line do |host_entry|
|
31
|
+
entries = host_entry.gsub(/#.*$/, '').gsub(/\s+/, ' ').split(' ')
|
32
|
+
addr = entries.shift
|
33
|
+
entries.each { |e| hosts[e] ||= addr }
|
34
|
+
end
|
35
|
+
|
36
|
+
hosts[host]
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(hostname, *nameservers)
|
40
|
+
if nameservers.nil? or nameservers.empty?
|
41
|
+
nameservers = File.read(RESOLV_CONF).scan(/^\s*nameserver\s+([0-9.:]+)/).flatten
|
42
|
+
raise RuntimeError, "no nameservers found in #{RESOLV_CONF}" if nameservers.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
@nameservers = nameservers
|
46
|
+
@request = request_message hostname
|
47
|
+
@question = request_question hostname
|
48
|
+
|
49
|
+
@socket = UDPSocket.new
|
50
|
+
@timer = Timeout.new(self)
|
51
|
+
super(@socket)
|
52
|
+
end
|
53
|
+
|
54
|
+
def attach(evloop)
|
55
|
+
send_request
|
56
|
+
@timer.attach(evloop)
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def detach
|
61
|
+
@timer.detach if @timer.attached?
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
# Send a request to the DNS server
|
66
|
+
def send_request
|
67
|
+
@socket.connect @nameservers.first, DNS_PORT
|
68
|
+
@socket.send @request, 0
|
69
|
+
end
|
70
|
+
|
71
|
+
# Called when the name has successfully resolved to an address
|
72
|
+
def on_success(address); end
|
73
|
+
event_callback :on_success
|
74
|
+
|
75
|
+
# Called when we receive a response indicating the name didn't resolve
|
76
|
+
def on_failure; end
|
77
|
+
event_callback :on_failure
|
78
|
+
|
79
|
+
# Called if we don't receive a response
|
80
|
+
def on_timeout; end
|
81
|
+
event_callback :on_timeout
|
82
|
+
|
83
|
+
#########
|
84
|
+
protected
|
85
|
+
#########
|
86
|
+
|
87
|
+
# Called by the subclass when the DNS response is available
|
88
|
+
def on_readable
|
89
|
+
datagram = @socket.recvfrom_nonblock(DATAGRAM_SIZE).first
|
90
|
+
address = response_address datagram rescue nil
|
91
|
+
address ? on_success(address) : on_failure
|
92
|
+
detach
|
93
|
+
end
|
94
|
+
|
95
|
+
def request_message(hostname)
|
96
|
+
# Standard query header
|
97
|
+
message = "\000\002\001\000"
|
98
|
+
|
99
|
+
# One entry
|
100
|
+
qdcount = 1
|
101
|
+
|
102
|
+
# No answer, authority, or additional records
|
103
|
+
ancount = nscount = arcount = 0
|
104
|
+
|
105
|
+
message << [qdcount, ancount, nscount, arcount].pack('nnnn')
|
106
|
+
message << request_question(hostname)
|
107
|
+
end
|
108
|
+
|
109
|
+
def request_question(hostname)
|
110
|
+
# Query name
|
111
|
+
message = hostname.split('.').map { |s| [s.size].pack('C') << s }.join + "\000"
|
112
|
+
|
113
|
+
# Host address query
|
114
|
+
qtype = 1
|
115
|
+
|
116
|
+
# Internet query
|
117
|
+
qclass = 1
|
118
|
+
|
119
|
+
message << [qtype, qclass].pack('nn')
|
120
|
+
end
|
121
|
+
|
122
|
+
def response_address(message)
|
123
|
+
# Confirm the ID field
|
124
|
+
id = message[0..1].unpack('n').first.to_i
|
125
|
+
return unless id == 2
|
126
|
+
|
127
|
+
# Check the QR value and confirm this message is a response
|
128
|
+
qr = message[2].unpack('B1').first.to_i
|
129
|
+
return unless qr == 1
|
130
|
+
|
131
|
+
# Check the RCODE and ensure there wasn't an error
|
132
|
+
rcode = message[3].unpack('B8').first[4..7].to_i(2)
|
133
|
+
return unless rcode == 0
|
134
|
+
|
135
|
+
# Extract the question and answer counts
|
136
|
+
qdcount, ancount = message[4..7].unpack('nn').map { |n| n.to_i }
|
137
|
+
|
138
|
+
# We only asked one question
|
139
|
+
return unless qdcount == 1
|
140
|
+
message = message[12..message.size] # slice! would be nice but I think I hit a 1.9 bug...
|
141
|
+
|
142
|
+
# Make sure it's the same question
|
143
|
+
return unless message[0..(@question.size-1)] == @question
|
144
|
+
message = message[@question.size..message.size]
|
145
|
+
|
146
|
+
# Extract the RDLENGTH
|
147
|
+
while not message.empty?
|
148
|
+
type = message[2..3].unpack('n').first.to_i
|
149
|
+
rdlength = message[10..11].unpack('n').first.to_i
|
150
|
+
rdata = message[12..(12 + rdlength - 1)]
|
151
|
+
message = message[(12+rdlength)..message.size]
|
152
|
+
|
153
|
+
# Only IPv4 supported
|
154
|
+
next unless rdlength == 4
|
155
|
+
|
156
|
+
return rdata.unpack('CCCC').join('.') if type == 1
|
157
|
+
end
|
158
|
+
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
|
162
|
+
class Timeout < TimerWatcher
|
163
|
+
def initialize(resolver)
|
164
|
+
@resolver = resolver
|
165
|
+
@attempts = 0
|
166
|
+
super(TIMEOUT, true)
|
167
|
+
end
|
168
|
+
|
169
|
+
def on_timer
|
170
|
+
@attempts += 1
|
171
|
+
return @resolver.send_request if @attempts <= RETRIES
|
172
|
+
|
173
|
+
@resolver.on_timeout
|
174
|
+
@resolver.detach
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../rev'
|
8
|
+
|
9
|
+
module Rev
|
10
|
+
class IOWatcher
|
11
|
+
# The actual implementation of this class resides in the C extension
|
12
|
+
# Here we metaprogram proper event_callbacks for the callback methods
|
13
|
+
# These can take a block and store it to be called when the event
|
14
|
+
# is actually fired.
|
15
|
+
|
16
|
+
event_callback :on_readable, :on_writable
|
17
|
+
end
|
18
|
+
end
|
data/lib/rev/listener.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'socket'
|
8
|
+
require File.dirname(__FILE__) + '/../rev'
|
9
|
+
|
10
|
+
module Rev
|
11
|
+
class Listener < IOWatcher
|
12
|
+
def initialize(listen_socket)
|
13
|
+
@listen_socket = listen_socket
|
14
|
+
super(@listen_socket)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Called whenever the server receives a new connection
|
18
|
+
def on_connection(socket); end
|
19
|
+
event_callback :on_connection
|
20
|
+
|
21
|
+
#########
|
22
|
+
protected
|
23
|
+
#########
|
24
|
+
|
25
|
+
# Rev callback for handling new connections
|
26
|
+
def on_readable
|
27
|
+
on_connection @listen_socket.accept_nonblock
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class TCPListener < Listener
|
32
|
+
# Create a new Rev::TCPListener
|
33
|
+
#
|
34
|
+
# Accepts the same arguments as TCPServer.new
|
35
|
+
def initialize(*args)
|
36
|
+
listen_socket = ::TCPServer.new(*args)
|
37
|
+
listen_socket.instance_eval { listen(1024) } # Change listen backlog to 1024
|
38
|
+
super(listen_socket)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class UNIXListener < Listener
|
43
|
+
# Create a new Rev::UNIXListener
|
44
|
+
#
|
45
|
+
# Accepts the same arguments as UNIXServer.new
|
46
|
+
def initialize(*args)
|
47
|
+
super(::UNIXServer.new(*args))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|