rev 0.1.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/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
|