ktools 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +4 -3
- data/Rakefile +5 -2
- data/ktools.gemspec +3 -2
- data/lib/ktools/epoll.rb +160 -2
- data/lib/ktools/kqueue.rb +21 -11
- data/tests/test_epoll.rb +41 -14
- data/tests/test_kqueue.rb +0 -3
- metadata +12 -2
data/README
CHANGED
@@ -23,13 +23,14 @@ Synopsis:
|
|
23
23
|
I plan to support the following kernel APIs:
|
24
24
|
|
25
25
|
kqueue (works - see tests/test_kqueue.rb)
|
26
|
-
epoll (works - see tests/test_epoll.rb
|
26
|
+
epoll (works - see tests/test_epoll.rb)
|
27
27
|
inotify
|
28
28
|
netlink
|
29
29
|
|
30
30
|
and maybe some others! I will at first hook up the C interfaces as directly
|
31
|
-
as possible, and then write Rubyist-friendly wrappers around them.
|
32
|
-
|
31
|
+
as possible, and then write Rubyist-friendly wrappers around them.
|
32
|
+
|
33
|
+
Currently kqueue and epoll have Ruby wrapper APIs.
|
33
34
|
|
34
35
|
|
35
36
|
To install:
|
data/Rakefile
CHANGED
@@ -57,9 +57,12 @@ task :clean do
|
|
57
57
|
end
|
58
58
|
|
59
59
|
task :test do
|
60
|
+
require 'rubygems'
|
60
61
|
require 'lib/ktools'
|
61
|
-
|
62
|
-
|
62
|
+
require 'bacon'
|
63
|
+
Bacon.summary_on_exit
|
64
|
+
load "tests/test_kqueue.rb" if Kernel.have_kqueue?
|
65
|
+
load "tests/test_epoll.rb" if Kernel.have_epoll?
|
63
66
|
end
|
64
67
|
|
65
68
|
task :objs do
|
data/ktools.gemspec
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "ktools"
|
3
|
-
s.version = "0.0.
|
4
|
-
s.date = "2009-04-
|
3
|
+
s.version = "0.0.2"
|
4
|
+
s.date = "2009-04-22"
|
5
5
|
s.authors = ["Jake Douglas"]
|
6
6
|
s.email = "jakecdouglas@gmail.com"
|
7
7
|
s.rubyforge_project = "ktools"
|
8
8
|
s.has_rdoc = true
|
9
9
|
s.add_dependency('ffi')
|
10
|
+
s.add_dependency('bacon')
|
10
11
|
s.summary = "Bringing common kernel APIs into Ruby using FFI"
|
11
12
|
s.homepage = "http://www.github.com/yakischloba/ktools"
|
12
13
|
s.description = "Bringing common kernel APIs into Ruby using FFI"
|
data/lib/ktools/epoll.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Kernel
|
2
|
-
|
2
|
+
class Epoll
|
3
3
|
extend FFI::Library
|
4
4
|
|
5
5
|
class Epoll_data < FFI::Struct
|
@@ -11,7 +11,11 @@ module Kernel
|
|
11
11
|
|
12
12
|
class Epoll_event < FFI::Struct
|
13
13
|
layout :events, :uint32,
|
14
|
-
:data,
|
14
|
+
:data, :pointer
|
15
|
+
|
16
|
+
def [] key
|
17
|
+
key == :data ? Epoll_data.new(super(key)) : super(key)
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
21
|
epc = FFI::ConstGenerator.new do |c|
|
@@ -26,6 +30,7 @@ module Kernel
|
|
26
30
|
c.const("EPOLLMSG")
|
27
31
|
c.const("EPOLLERR")
|
28
32
|
c.const("EPOLLHUP")
|
33
|
+
c.const("EPOLLRDHUP")
|
29
34
|
c.const("EPOLLONESHOT")
|
30
35
|
c.const("EPOLLET")
|
31
36
|
c.const("EPOLL_CTL_ADD")
|
@@ -35,11 +40,164 @@ module Kernel
|
|
35
40
|
|
36
41
|
eval epc.to_ruby
|
37
42
|
|
43
|
+
EP_FLAGS = {
|
44
|
+
:read => EPOLLIN,
|
45
|
+
:write => EPOLLOUT,
|
46
|
+
:hangup => EPOLLHUP,
|
47
|
+
:priority => EPOLLPRI,
|
48
|
+
:edge => EPOLLET,
|
49
|
+
:oneshot => EPOLLONESHOT,
|
50
|
+
:error => EPOLLERR
|
51
|
+
}
|
52
|
+
|
53
|
+
EP_FLAGS[:remote_hangup] = EPOLLRDHUP if const_defined?("EPOLLRDHUP")
|
54
|
+
|
38
55
|
# Attach directly to epoll_create
|
39
56
|
attach_function :epoll_create, [:int], :int
|
40
57
|
# Attach directly to epoll_ctl
|
41
58
|
attach_function :epoll_ctl, [:int, :int, :int, :pointer], :int
|
42
59
|
# Attach to the epoll_wait wrapper so we can use rb_thread_blocking_region when possible
|
43
60
|
attach_function :epoll_wait, :wrap_epoll_wait, [:int, :pointer, :int, :int], :int
|
61
|
+
|
62
|
+
# Creates a new epoll event queue. Takes an optional size parameter (default 1024) that is a hint
|
63
|
+
# to the kernel about how many descriptors it will be handling. Read man epoll_create for details
|
64
|
+
# on this. Raises an error if the operation fails.
|
65
|
+
def initialize(size=1024)
|
66
|
+
@fds = {}
|
67
|
+
@epfd = epoll_create(size)
|
68
|
+
raise SystemCallError.new("Error creating epoll descriptor", get_errno) unless @epfd > 0
|
69
|
+
end
|
70
|
+
|
71
|
+
# Generic method for adding events. This simply calls the proper add_foo method specified by the type symbol.
|
72
|
+
# Example:
|
73
|
+
# ep.add(:socket, sock, :events => [:read])
|
74
|
+
# calls -> ep.add_socket(sock, events => [:read])
|
75
|
+
#
|
76
|
+
# Note: even though epoll only supports :socket style descriptors, we keep this for consistency with other APIs.
|
77
|
+
def add(type, target, options={})
|
78
|
+
case type
|
79
|
+
when :socket
|
80
|
+
add_socket(target, options)
|
81
|
+
else
|
82
|
+
raise ArgumentError.new("Epoll only supports socket style descriptors")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add events to a socket-style descriptor (socket or pipe). Your target can be either
|
87
|
+
# an IO object (socket, pipe), or a file descriptor number.
|
88
|
+
#
|
89
|
+
# Supported :events are:
|
90
|
+
#
|
91
|
+
# * :read - The descriptor has become readable.
|
92
|
+
# * :write - The descriptor has become writeable.
|
93
|
+
# * :priority - There is urgent data available for read operations.
|
94
|
+
# * :error - Error condition happened on the associated file descriptor. (Always active)
|
95
|
+
# * :hangup - Hang up happened on the associated file descriptor. (Always active)
|
96
|
+
# * :remote_hangup - Stream socket peer closed the connection, or shut down writing half of connection. (Missing from some kernel verions)
|
97
|
+
#
|
98
|
+
# Supported :flags are:
|
99
|
+
#
|
100
|
+
# * :edge - Sets the Edge Triggered behavior for the associated file descriptor. (see manpage)
|
101
|
+
# * :oneshot - Sets the one-shot behaviour for the associated file descriptor. (Event only fires once)
|
102
|
+
#
|
103
|
+
# Example:
|
104
|
+
#
|
105
|
+
# irb(main):001:0> require 'ktools'
|
106
|
+
# => true
|
107
|
+
# irb(main):002:0> r, w = IO.pipe
|
108
|
+
# => [#<IO:0x89be38c>, #<IO:0x89be378>]
|
109
|
+
# irb(main):003:0> ep = Epoll.new
|
110
|
+
# => #<Kernel::Epoll:0x89bca3c @fds={}, @epfd=5>
|
111
|
+
# irb(main):004:0> ep.add(:socket, r, :events => [:read])
|
112
|
+
# => true
|
113
|
+
# irb(main):005:0> ep.poll
|
114
|
+
# => []
|
115
|
+
# irb(main):006:0> w.write 'foo'
|
116
|
+
# => 3
|
117
|
+
# irb(main):007:0> ep.poll
|
118
|
+
# => [{:target=>#<IO:0x89be38c>, :event=>:read, :type=>:socket}]
|
119
|
+
# irb(main):008:0> [r, w, ep].each{|x| x.close }
|
120
|
+
def add_socket(target, options={})
|
121
|
+
fdnum = target.respond_to?(:fileno) ? target.fileno : target
|
122
|
+
events = (options[:events] + (options[:flags] || [])).inject(0){|m,i| m | EP_FLAGS[i]}
|
123
|
+
|
124
|
+
ev = Epoll_event.new
|
125
|
+
ev[:events] = events
|
126
|
+
ev[:data] = Epoll_data.new
|
127
|
+
ev[:data][:fd] = fdnum
|
128
|
+
|
129
|
+
if epoll_ctl(@epfd, EPOLL_CTL_ADD, fdnum, ev) == -1
|
130
|
+
return false
|
131
|
+
else
|
132
|
+
@fds[fdnum] = {:target => target, :event => ev}
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Poll for an event. Pass an optional timeout float as number of seconds to wait for an event. Default is 0.0 (do not wait).
|
138
|
+
#
|
139
|
+
# Using a timeout will block for the duration of the timeout. Under Ruby 1.9.1, we use rb_thread_blocking_region() under the
|
140
|
+
# hood to allow other threads to run during this call. Prior to 1.9 though, we do not have native threads and hence this call
|
141
|
+
# will block the whole interpreter (all threads) until it returns.
|
142
|
+
#
|
143
|
+
# This call returns an array of hashes, similar to the following:
|
144
|
+
# => [{:type=>:socket, :target=>#<IO:0x4fa90c>, :event=>:read}]
|
145
|
+
#
|
146
|
+
# * :type - will be the type of event target, i.e. an event set with #add_socket will have :type => :socket
|
147
|
+
# * :target - the 'target' or 'subject' of the event. This can be a File, IO, process or signal number.
|
148
|
+
# * :event - the event that occurred on the target. This is one of the symbols you passed as :events => [:foo] when adding the event.
|
149
|
+
#
|
150
|
+
# Note: even though epoll only supports :socket style descriptors, we keep :type for consistency with other APIs.
|
151
|
+
def poll(timeout=0.0)
|
152
|
+
timeout = (timeout * 1000).to_i
|
153
|
+
ev = Epoll_event.new
|
154
|
+
case epoll_wait(@epfd, ev, 1, timeout)
|
155
|
+
when -1
|
156
|
+
[errno]
|
157
|
+
when 0
|
158
|
+
[]
|
159
|
+
else
|
160
|
+
[process_event(ev)]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def process_event(ev) #:nodoc:
|
165
|
+
h = @fds[ev[:data][:fd]]
|
166
|
+
return nil if h.nil?
|
167
|
+
|
168
|
+
event = if ev[:events] & EPOLLIN == EPOLLIN
|
169
|
+
:read
|
170
|
+
elsif ev[:events] & EPOLLOUT == EPOLLOUT
|
171
|
+
:write
|
172
|
+
elsif ev[:events] & ERPOLLPRI == EPOLLPRI
|
173
|
+
:priority
|
174
|
+
elsif ev[:events] & EPOLLERR == EPOLLERR
|
175
|
+
:error
|
176
|
+
elsif ev[:events] & EPOLLHUP == EPOLLHUP
|
177
|
+
:hangup
|
178
|
+
elsif Epoll.const_defined?("EPOLLRDHUP") and ev[:events] & EPOLLRDHUP == EPOLLRDHUP
|
179
|
+
:remote_hangup
|
180
|
+
end
|
181
|
+
|
182
|
+
delete(:socket, h[:target]) if ev[:events] & EPOLLONESHOT == EPOLLONESHOT
|
183
|
+
{:target => h[:target], :event => event, :type => :socket}
|
184
|
+
end
|
185
|
+
|
186
|
+
# Stop generating events for the given type and event target, ie:
|
187
|
+
# ep.delete(:socket, sock)
|
188
|
+
#
|
189
|
+
# Note: even though epoll only supports :socket style descriptors, we keep this for consistency with other APIs.
|
190
|
+
def delete(type, target)
|
191
|
+
ident = target.respond_to?(:fileno) ? target.fileno : target
|
192
|
+
h = @fds[ident]
|
193
|
+
return false if h.nil?
|
194
|
+
epoll_ctl(@epfd, EPOLL_CTL_DEL, ident, h[:event])
|
195
|
+
return true
|
196
|
+
end
|
197
|
+
|
198
|
+
def close
|
199
|
+
IO.for_fd(@epfd).close
|
200
|
+
end
|
201
|
+
|
44
202
|
end
|
45
203
|
end
|
data/lib/ktools/kqueue.rb
CHANGED
@@ -115,7 +115,7 @@ module Kernel
|
|
115
115
|
|
116
116
|
# We provide the raw C interface above. Now we OO-ify it.
|
117
117
|
|
118
|
-
# Creates a new kqueue
|
118
|
+
# Creates a new kqueue event queue. Will raise an error if the operation fails.
|
119
119
|
def initialize
|
120
120
|
@fds = {}
|
121
121
|
@pids = {}
|
@@ -137,13 +137,18 @@ module Kernel
|
|
137
137
|
add_process(target, options)
|
138
138
|
when :signal
|
139
139
|
add_signal(target, options)
|
140
|
+
else
|
141
|
+
raise ArgumentError.new("Unknown event type #{type}")
|
140
142
|
end
|
141
143
|
end
|
142
144
|
|
143
145
|
# Add events on a file to the Kqueue. kqueue requires that a file actually be opened (to get a descriptor)
|
144
146
|
# before it can be monitored. You can pass a File object here, or a String of the pathname, in which
|
145
147
|
# case we'll try to open the file for you. In either case, a File object will be returned as the :target
|
146
|
-
# in the event returned by #poll.
|
148
|
+
# in the event returned by #poll. If you want to keep track of it yourself, you can just pass the file
|
149
|
+
# descriptor number (and that's what you'll get back.)
|
150
|
+
#
|
151
|
+
# Valid events here are as follows, using descriptions from the kqueue man pages:
|
147
152
|
#
|
148
153
|
# * :delete - "The unlink() system call was called on the file referenced by the descriptor."
|
149
154
|
# * :write - "A write occurred on the file referenced by the descriptor."
|
@@ -174,22 +179,26 @@ module Kernel
|
|
174
179
|
fflags, flags = options.values_at :events, :flags
|
175
180
|
raise ArgumentError.new("must specify which file events to watch for") unless fflags
|
176
181
|
|
177
|
-
file = file.kind_of?(
|
182
|
+
file = file.kind_of?(String) ? File.open(file, 'r') : file
|
183
|
+
fdnum = file.respond_to?(:fileno) ? file.fileno : file
|
178
184
|
|
179
185
|
k = Kevent.new
|
180
186
|
flags = flags ? flags.inject(0){|m,i| m | KQ_FLAGS[i] } : EV_CLEAR
|
181
187
|
fflags = fflags.inject(0){|m,i| m | KQ_FFLAGS[i] }
|
182
|
-
ev_set(k,
|
188
|
+
ev_set(k, fdnum, EVFILT_VNODE, EV_ADD | flags, fflags, 0, nil)
|
183
189
|
|
184
190
|
if kevent(@kqfd, k, 1, nil, 0, nil) == -1
|
185
191
|
return false
|
186
192
|
else
|
187
|
-
@fds[
|
193
|
+
@fds[fdnum] = {:target => file, :event => k}
|
188
194
|
return true
|
189
195
|
end
|
190
196
|
end
|
191
197
|
|
192
|
-
# Add events to a socket-style descriptor (socket or pipe).
|
198
|
+
# Add events to a socket-style descriptor (socket or pipe). Your target can be either
|
199
|
+
# an IO object (socket, pipe), or a file descriptor number.
|
200
|
+
#
|
201
|
+
# Supported events are:
|
193
202
|
#
|
194
203
|
# * :read - The descriptor has become readable.
|
195
204
|
# * :write - The descriptor has become writeable.
|
@@ -213,18 +222,19 @@ module Kernel
|
|
213
222
|
# irb(main):007:0> kq.poll
|
214
223
|
# => [{:type=>:socket, :target=>#<IO:0x4fa90c>, :event=>:read}]
|
215
224
|
# irb(main):008:0> [r, w, kq].each {|i| i.close}
|
216
|
-
def add_socket(
|
225
|
+
def add_socket(target, options={})
|
217
226
|
filters, flags = options.values_at :events, :flags
|
218
227
|
flags = flags ? flags.inject(0){|m,i| m | KQ_FLAGS[i] } : EV_CLEAR
|
219
228
|
filters = filters ? filters.inject(0){|m,i| m | KQ_FILTERS[i] } : EVFILT_READ | EVFILT_WRITE
|
229
|
+
fdnum = target.respond_to?(:fileno) ? target.fileno : target
|
220
230
|
|
221
231
|
k = Kevent.new
|
222
|
-
ev_set(k,
|
232
|
+
ev_set(k, fdnum, filters, EV_ADD | flags, 0, 0, nil)
|
223
233
|
|
224
234
|
if kevent(@kqfd, k, 1, nil, 0, nil) == -1
|
225
235
|
return false
|
226
236
|
else
|
227
|
-
@fds[
|
237
|
+
@fds[fdnum] = {:target => target, :event => k}
|
228
238
|
return true
|
229
239
|
end
|
230
240
|
end
|
@@ -263,7 +273,7 @@ module Kernel
|
|
263
273
|
if kevent(@kqfd, k, 1, nil, 0, nil) == -1
|
264
274
|
return false
|
265
275
|
else
|
266
|
-
@pids[pid] = {:target => pid, :
|
276
|
+
@pids[pid] = {:target => pid, :event => k}
|
267
277
|
return true
|
268
278
|
end
|
269
279
|
end
|
@@ -362,7 +372,7 @@ module Kernel
|
|
362
372
|
end
|
363
373
|
h = container[ident]
|
364
374
|
return false if h.nil?
|
365
|
-
k = h[:
|
375
|
+
k = h[:event]
|
366
376
|
ev_set(k, k[:ident], k[:filter], EV_DELETE, k[:fflags], 0, nil)
|
367
377
|
kevent(@kqfd, k, 1, nil, 0, nil)
|
368
378
|
container.delete(ident)
|
data/tests/test_epoll.rb
CHANGED
@@ -1,29 +1,24 @@
|
|
1
|
-
require 'lib/ktools'
|
2
|
-
require 'bacon'
|
3
|
-
|
4
|
-
include Kernel::Epoll
|
5
|
-
|
6
1
|
describe "the epoll interface" do
|
7
2
|
|
8
3
|
it "should return a valid epoll file descriptor" do
|
9
|
-
@epfd = epoll_create(10)
|
4
|
+
@epfd = Epoll::epoll_create(10)
|
10
5
|
@epfd.class.should.equal Fixnum
|
11
6
|
@epfd.should.be > 0
|
12
7
|
end
|
13
8
|
|
14
9
|
it "should set an event for readable status of a descriptor, and retrieve the event when it occurs" do
|
15
10
|
r, w = IO.pipe
|
16
|
-
ev = Epoll_event.new
|
17
|
-
ev[:events] = EPOLLIN
|
18
|
-
ev[:data] = Epoll_data.new
|
11
|
+
ev = Epoll::Epoll_event.new
|
12
|
+
ev[:events] = Epoll::EPOLLIN
|
13
|
+
ev[:data] = Epoll::Epoll_data.new
|
19
14
|
ev[:data][:fd] = r.fileno
|
20
15
|
ev[:data][:u32] = 12345
|
21
|
-
epoll_ctl(@epfd, EPOLL_CTL_ADD, r.fileno, ev).should.equal 0
|
22
|
-
rev = Epoll_event.new
|
23
|
-
epoll_wait(@epfd, rev, 1, 50).should.equal 0
|
16
|
+
Epoll::epoll_ctl(@epfd, Epoll::EPOLL_CTL_ADD, r.fileno, ev).should.equal 0
|
17
|
+
rev = Epoll::Epoll_event.new
|
18
|
+
Epoll::epoll_wait(@epfd, rev, 1, 50).should.equal 0
|
24
19
|
w.write "foo"
|
25
|
-
epoll_wait(@epfd, rev, 1, 50).should.equal 1
|
26
|
-
rev[:events].should.equal EPOLLIN
|
20
|
+
Epoll::epoll_wait(@epfd, rev, 1, 50).should.equal 1
|
21
|
+
rev[:events].should.equal Epoll::EPOLLIN
|
27
22
|
rev[:data][:fd].should.equal r.fileno
|
28
23
|
rev[:data][:u32].should.equal 12345
|
29
24
|
w.close
|
@@ -39,4 +34,36 @@ describe "the epoll interface" do
|
|
39
34
|
i.should.be.closed
|
40
35
|
end
|
41
36
|
|
37
|
+
it "should add socket events and retrieve them using the Ruby API" do
|
38
|
+
r, w = IO.pipe
|
39
|
+
ep = Epoll.new
|
40
|
+
|
41
|
+
ep.add(:socket, r, :events => [:read]).should.be.true
|
42
|
+
ep.poll.should.be.empty
|
43
|
+
|
44
|
+
w.write 'foo'
|
45
|
+
res = ep.poll.first
|
46
|
+
|
47
|
+
res[:event].should.equal :read
|
48
|
+
res[:target].fileno.should.equal r.fileno
|
49
|
+
res[:type].should.equal :socket
|
50
|
+
|
51
|
+
[r,w,ep].each{|i| i.close}
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should delete events using the Ruby API" do
|
55
|
+
r, w = IO.pipe
|
56
|
+
ep = Epoll.new
|
57
|
+
|
58
|
+
ep.add(:socket, r, :events => [:read]).should.be.true
|
59
|
+
ep.poll.should.be.empty
|
60
|
+
|
61
|
+
w.write 'foo'
|
62
|
+
ep.delete(:socket, r).should.be.true
|
63
|
+
|
64
|
+
ep.poll.should.be.empty
|
65
|
+
|
66
|
+
[r,w,ep].each{|i| i.close}
|
67
|
+
end
|
68
|
+
|
42
69
|
end
|
data/tests/test_kqueue.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ktools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jake Douglas
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-04-
|
12
|
+
date: 2009-04-22 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -22,6 +22,16 @@ dependencies:
|
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: "0"
|
24
24
|
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bacon
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
25
35
|
description: Bringing common kernel APIs into Ruby using FFI
|
26
36
|
email: jakecdouglas@gmail.com
|
27
37
|
executables: []
|