ktools 0.0.1 → 0.0.2
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/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: []
|