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.
@@ -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
@@ -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
@@ -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