rev 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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