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 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. Needs Ruby wrapper.)
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. Kqueue
32
- currently has a (I think) decent wrapper and epoll is up next.
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
- sh "bacon tests/test_kqueue.rb" if Kernel.have_kqueue?
62
- sh "bacon tests/test_epoll.rb" if Kernel.have_epoll?
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
@@ -1,12 +1,13 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "ktools"
3
- s.version = "0.0.1"
4
- s.date = "2009-04-21"
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"
@@ -1,5 +1,5 @@
1
1
  module Kernel
2
- module Epoll
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, Epoll_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
@@ -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 object. Will raise an error if the operation fails.
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. Valid events here are as follows, using descriptions from the kqueue man pages:
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?(File) || file.kind_of?(Tempfile) ? file : File.open(file, 'r')
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, file.fileno, EVFILT_VNODE, EV_ADD | flags, fflags, 0, nil)
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[file.fileno] = {:target => file, :kevent => k}
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). Supported events are:
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(io, options={})
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, io.fileno, filters, EV_ADD | flags, 0, 0, nil)
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[io.fileno] = {:target => io, :kevent => k}
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, :kevent => k}
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[:kevent]
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)
@@ -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
@@ -1,6 +1,3 @@
1
- require 'lib/ktools'
2
- require 'bacon'
3
-
4
1
  describe "the kqueue interface" do
5
2
 
6
3
  it "should return a valid kqueue file descriptor" do
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.1
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-21 00:00:00 -07:00
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: []