event_core 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/event_core.rb +727 -0
- metadata +44 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 90ba19576aec87f0baf62d7b0f5f5e5cc8051f59
|
|
4
|
+
data.tar.gz: 48346c5c63c82787a530ea2fffbcb4e664544ae5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b78dea2f80278deb5650a14c7d54340dc8c12ccaec8be9a19ec0218df6c2e2037f65a2079ace6fa97cca5a5fa8036e547972432ec763bed4c710bda78999dde6
|
|
7
|
+
data.tar.gz: 16736f777a58f8041bf88b9c886684edd1cfd754e6adc5b1e6ad1a757c87e99850bec3c105018e658643b4e024428fd8145985eb84faa9ab105fa9c4e3da04ca
|
data/lib/event_core.rb
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
require 'fcntl'
|
|
2
|
+
require 'monitor'
|
|
3
|
+
require 'thread'
|
|
4
|
+
require 'fiber'
|
|
5
|
+
|
|
6
|
+
# TODO:
|
|
7
|
+
# - Maybe a super simple event bus
|
|
8
|
+
# - unit tests for error reporting in add_read/write
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
module EventCore
|
|
13
|
+
|
|
14
|
+
# Low level event source representation.
|
|
15
|
+
# Only needed when the convenience APIs on EventLoop are not enough.
|
|
16
|
+
class Source
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@closed = false
|
|
20
|
+
@ready = false
|
|
21
|
+
@timeout_secs = nil
|
|
22
|
+
@trigger = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if a source is ready. Called on each main loop iteration.
|
|
26
|
+
# May have side effects, but should not leave ready state until
|
|
27
|
+
# consume_event_data!() has been called.
|
|
28
|
+
def ready?
|
|
29
|
+
@ready
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Mark source as ready
|
|
33
|
+
def ready!(event_data=nil)
|
|
34
|
+
@ready = true
|
|
35
|
+
@event_data = event_data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Timeout in seconds, or nil
|
|
39
|
+
def timeout
|
|
40
|
+
@timeout_secs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# An optional IO object to select on
|
|
44
|
+
def select_io
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns :read, :write, or nil. If select_io is non-nil,
|
|
49
|
+
# then the select_type must not be nil.
|
|
50
|
+
def select_type
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Consume pending event data and set readiness to false
|
|
55
|
+
def consume_event_data!
|
|
56
|
+
raise "Source not ready: #{self}" unless ready?
|
|
57
|
+
data = @event_data
|
|
58
|
+
@event_data = nil
|
|
59
|
+
@ready = false
|
|
60
|
+
data
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Raw event data is passed to this function before passed to the trigger
|
|
64
|
+
def event_factory(event_data)
|
|
65
|
+
event_data
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check to see if close!() has been called.
|
|
69
|
+
def closed?
|
|
70
|
+
@closed
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Close this source, marking it for removal from the main loop.
|
|
74
|
+
def close!
|
|
75
|
+
@closed = true
|
|
76
|
+
@trigger = nil # Help the GC, if the closure holds onto some data
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set the trigger function to call on events to the given block
|
|
80
|
+
def trigger(&block)
|
|
81
|
+
@trigger = block
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Consume pending event data and fire the trigger,
|
|
85
|
+
# closing if the trigger returns (explicitly) false.
|
|
86
|
+
def notify_trigger
|
|
87
|
+
event_data = consume_event_data!
|
|
88
|
+
event = event_factory(event_data)
|
|
89
|
+
if @trigger
|
|
90
|
+
# Not just !@trigger.call(event), we want explicitly "false"
|
|
91
|
+
close! if @trigger.call(event) == false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Idle sources are triggered on each iteration of the event loop.
|
|
97
|
+
class IdleSource < Source
|
|
98
|
+
|
|
99
|
+
def initialize(event_data=nil)
|
|
100
|
+
super()
|
|
101
|
+
@ready = true
|
|
102
|
+
@event_data = event_data
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ready?
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def timeout
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def consume_event_data!
|
|
114
|
+
@event_data
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A source that triggers when data is ready to be read from an internal pipe.
|
|
120
|
+
# Send data to the pipe with the (blocking) write() method.
|
|
121
|
+
class PipeSource < Source
|
|
122
|
+
|
|
123
|
+
attr_reader :rio, :wio
|
|
124
|
+
|
|
125
|
+
def initialize
|
|
126
|
+
super()
|
|
127
|
+
@rio, @wio = IO.pipe
|
|
128
|
+
@rio.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC | Fcntl::O_NONBLOCK)
|
|
129
|
+
#@wio.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC | Fcntl::O_NONBLOCK)
|
|
130
|
+
@buffer_size = 4096
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def select_io
|
|
134
|
+
@rio
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def select_type
|
|
138
|
+
:read
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def consume_event_data!
|
|
142
|
+
begin
|
|
143
|
+
@rio.read_nonblock(@buffer_size)
|
|
144
|
+
rescue EOFError
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def closed?
|
|
150
|
+
@rio.closed?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def close!
|
|
154
|
+
super
|
|
155
|
+
@rio.close unless @rio.closed?
|
|
156
|
+
@wio.close unless @wio.closed?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def write(buf)
|
|
160
|
+
@wio.write(buf)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class IOSource < Source
|
|
166
|
+
|
|
167
|
+
attr_accessor :auto_close
|
|
168
|
+
|
|
169
|
+
def initialize(io, type)
|
|
170
|
+
super()
|
|
171
|
+
raise "Nil IO provided" if io.nil?
|
|
172
|
+
@io = io
|
|
173
|
+
@type = type
|
|
174
|
+
@auto_close = true
|
|
175
|
+
raise "Invalid select type: #{type}" unless [:read, :write].include?(type)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def select_io
|
|
179
|
+
@io
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def select_type
|
|
183
|
+
@type
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def consume_event_data!
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def close!
|
|
191
|
+
super
|
|
192
|
+
@io.close if @auto_close and not @io.closed?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Source for Ruby Fibers run on the mainloop.
|
|
198
|
+
# When a fiber yields it returns control to the mainloop.
|
|
199
|
+
# It supports async sub-tasks in sync-style programming by yielding Procs.
|
|
200
|
+
# See MainLoop.add_fiber for details.
|
|
201
|
+
class FiberSource < Source
|
|
202
|
+
|
|
203
|
+
def initialize(loop, proc=nil)
|
|
204
|
+
super()
|
|
205
|
+
raise "First arg must be a MainLoop: #{loop}" unless loop.is_a? MainLoop
|
|
206
|
+
raise "Second arg must be a Proc: #{proc.class}" unless (proc.is_a? Proc or proc.nil?)
|
|
207
|
+
|
|
208
|
+
@loop = loop
|
|
209
|
+
|
|
210
|
+
create_fiber(proc) if proc
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def ready!(event_data=nil)
|
|
214
|
+
super(event_data)
|
|
215
|
+
@loop.send_wakeup
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def consume_event_data!
|
|
219
|
+
event_data = super
|
|
220
|
+
@ready = true
|
|
221
|
+
event_data
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def close!
|
|
225
|
+
super
|
|
226
|
+
@fiber = nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def create_fiber(proc)
|
|
230
|
+
raise "Arg must be a Proc: #{proc.class}" unless proc.is_a? Proc
|
|
231
|
+
raise "Fiber already created for this source" if @fiber
|
|
232
|
+
|
|
233
|
+
@fiber = Fiber.new { proc.call }
|
|
234
|
+
|
|
235
|
+
@ready = true
|
|
236
|
+
|
|
237
|
+
trigger { |async_task_data|
|
|
238
|
+
task = @fiber.resume(async_task_data)
|
|
239
|
+
|
|
240
|
+
# If yielding, maybe spawn an async sub-task?
|
|
241
|
+
if @fiber.alive?
|
|
242
|
+
if task.is_a? Proc
|
|
243
|
+
raise "Fibers on the main loop must take exactly 1 argument. Proc takes #{task.arity}" unless task.arity == 1
|
|
244
|
+
@ready = false # don't fire again until task is done
|
|
245
|
+
@loop.add_once {
|
|
246
|
+
fiber_task = FiberTask.new(self)
|
|
247
|
+
task.call(fiber_task)
|
|
248
|
+
}
|
|
249
|
+
elsif task.nil?
|
|
250
|
+
# all good, just yielding until next loop iteration
|
|
251
|
+
else
|
|
252
|
+
raise "Fibers that yield must return nil or a Proc: #{task.class}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
next @fiber.alive?
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Encapsulates state of an async task spun off from a fiber.
|
|
264
|
+
class FiberTask
|
|
265
|
+
def initialize(fiber_source)
|
|
266
|
+
@fiber_source = fiber_source
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Mark yielded fiber ready for resumption. If the task has a result supply that as argument to done(),
|
|
270
|
+
# and it will become the result of the yield.
|
|
271
|
+
def done(result=nil)
|
|
272
|
+
@fiber_source.ready!(result)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# A source that marshals Unix signals to be handled in the main loop.
|
|
277
|
+
# This detaches you from the dreaded "trap context", and allows
|
|
278
|
+
# you to reason about the state of the rest of your app
|
|
279
|
+
# in the signal handler.
|
|
280
|
+
#
|
|
281
|
+
# The trigger is called with an array of signals as argument.
|
|
282
|
+
# There can be more than one signal if more than one signal fired
|
|
283
|
+
# since the source was last checked.
|
|
284
|
+
#
|
|
285
|
+
# Closing the signal handler will set the trap handler to DEFAULT.
|
|
286
|
+
class UnixSignalSource < PipeSource
|
|
287
|
+
|
|
288
|
+
# Give it a list of signals, names or integers, to listen for.
|
|
289
|
+
def initialize(*signals)
|
|
290
|
+
super()
|
|
291
|
+
@signals = signals
|
|
292
|
+
@signals.each do |sig|
|
|
293
|
+
Signal.trap(sig) do
|
|
294
|
+
write("#{sig}+")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def event_factory(event_data)
|
|
300
|
+
# We may have received more than one signal since last check
|
|
301
|
+
event_data.split('+')
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def close!
|
|
305
|
+
super
|
|
306
|
+
# Restore default signal handlers
|
|
307
|
+
@signals.each { |sig| Signal.trap(sig, "DEFAULT")}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A source that fires the trigger depending on a timeout.
|
|
313
|
+
class TimeoutSource < Source
|
|
314
|
+
def initialize(secs)
|
|
315
|
+
super()
|
|
316
|
+
@timeout_secs = secs
|
|
317
|
+
@next_timestamp = Time.now.to_f + secs
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def ready?
|
|
321
|
+
return true if @ready
|
|
322
|
+
|
|
323
|
+
now = Time.now.to_f
|
|
324
|
+
if now >= @next_timestamp
|
|
325
|
+
ready!
|
|
326
|
+
@next_timestamp = now + @timeout_secs
|
|
327
|
+
return true
|
|
328
|
+
end
|
|
329
|
+
false
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def timeout
|
|
333
|
+
delta = @next_timestamp - Time.now.to_f
|
|
334
|
+
delta > 0 ? delta : 0
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Core data structure for handling and polling Sources.
|
|
339
|
+
class MainLoop
|
|
340
|
+
|
|
341
|
+
def initialize
|
|
342
|
+
# We use a monitor, not a mutex, becuase Ruby mutexes are not reentrant,
|
|
343
|
+
# and we need reentrancy to be able to add sources from within trigger callbacks
|
|
344
|
+
@monitor = Monitor.new
|
|
345
|
+
|
|
346
|
+
@monitor.synchronize {
|
|
347
|
+
@sources = []
|
|
348
|
+
@quit_handlers = []
|
|
349
|
+
|
|
350
|
+
# Only ever set @do_quit through the quit() method!
|
|
351
|
+
# Otherwise the state of the loop will be undefiend
|
|
352
|
+
@do_quit = false
|
|
353
|
+
@control_source = PipeSource.new
|
|
354
|
+
@control_source.trigger { |event|
|
|
355
|
+
# We can get multiple control messages in one event,
|
|
356
|
+
# so generally it is a "string of control chars", hence the include? and not ==
|
|
357
|
+
# If event is nil, it means the pipe has been closed
|
|
358
|
+
@do_quit = true if event.nil? || event.include?('q')
|
|
359
|
+
}
|
|
360
|
+
@sources << @control_source
|
|
361
|
+
|
|
362
|
+
@sigchld_source = nil
|
|
363
|
+
@children = []
|
|
364
|
+
|
|
365
|
+
@thread = nil
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Add an event source to check in the loop. You can do this from any thread,
|
|
370
|
+
# or from trigger callbacks, or whenever you please.
|
|
371
|
+
# Returns the source, so you can close!() it when no longer needed.
|
|
372
|
+
def add_source(source)
|
|
373
|
+
@monitor.synchronize {
|
|
374
|
+
wakeup_needed = !@thread.nil? && @thread != Thread.current
|
|
375
|
+
raise "Unable to add source - loop terminated" if @sources.nil?
|
|
376
|
+
@sources << source
|
|
377
|
+
send_wakeup if wakeup_needed
|
|
378
|
+
}
|
|
379
|
+
source
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Add an idle callback to the loop. Will be removed like any other
|
|
383
|
+
# if it returns with 'next false'.
|
|
384
|
+
# For one-off dispatches into the main loop, fx. for callbacks from
|
|
385
|
+
# another thread add_once() is even more convenient.
|
|
386
|
+
# Returns the source, so you can close!() it when no longer needed.
|
|
387
|
+
def add_idle(&block)
|
|
388
|
+
source = IdleSource.new
|
|
389
|
+
source.trigger { next false if block.call == false }
|
|
390
|
+
add_source(source)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Add an idle callback that is removed after its first invocation,
|
|
394
|
+
# no matter how it returns.
|
|
395
|
+
# Returns the source, for API consistency, but it is not really useful,
|
|
396
|
+
# as it will be auto-closed on next mainloop iteration.
|
|
397
|
+
def add_once(delay_secs=nil, &block)
|
|
398
|
+
source = delay_secs.nil? ? IdleSource.new : TimeoutSource.new(delay_secs)
|
|
399
|
+
source.trigger { block.call; next false }
|
|
400
|
+
add_source(source)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Add a timeout function to be called periodically, or until it returns with 'next false'.
|
|
404
|
+
# The timeout is in seconds and the first call is fired after it has elapsed.
|
|
405
|
+
# Returns the source, so you can close!() it when no longer needed.
|
|
406
|
+
def add_timeout(secs, &block)
|
|
407
|
+
source = TimeoutSource.new(secs)
|
|
408
|
+
source.trigger { next false if block.call == false }
|
|
409
|
+
add_source(source)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Add a unix signal handler that is dispatched in the main loop.
|
|
413
|
+
# The handler will receive an array of signal numbers that was triggered
|
|
414
|
+
# since last step in the loop. You can provide one or more signals
|
|
415
|
+
# to listen for, given as integers or names.
|
|
416
|
+
# Returns the source, so you can close!() it when no longer needed.
|
|
417
|
+
def add_unix_signal(*signals, &block)
|
|
418
|
+
source = UnixSignalSource.new(*signals)
|
|
419
|
+
source.trigger { |signals| next false if block.call(signals) == false }
|
|
420
|
+
add_source(source)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Asynchronously write buf to io. Invokes block when complete,
|
|
424
|
+
# giving any encountered exception as argument, nil on success.
|
|
425
|
+
# Returns the source so you can close! it to cancel.
|
|
426
|
+
def add_write(io, buf, &block)
|
|
427
|
+
source = IOSource.new(io, :write)
|
|
428
|
+
source.trigger {
|
|
429
|
+
begin
|
|
430
|
+
# Note: because of string encoding snafu, Ruby can report more bytes read than buf.length!
|
|
431
|
+
len = io.write_nonblock(buf)
|
|
432
|
+
if len == buf.bytesize
|
|
433
|
+
block.call(nil) unless block.nil?
|
|
434
|
+
next false
|
|
435
|
+
end
|
|
436
|
+
buf = buf.byteslice(len..-1)
|
|
437
|
+
next true
|
|
438
|
+
rescue IO::WaitWritable
|
|
439
|
+
# All good, wait until we're writable again
|
|
440
|
+
next true
|
|
441
|
+
rescue => e
|
|
442
|
+
block.call(e) unless block.nil?
|
|
443
|
+
next false
|
|
444
|
+
end
|
|
445
|
+
}
|
|
446
|
+
add_source(source)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Asynchronously read an IO calling the block each time data is ready.
|
|
450
|
+
# The block receives to arguments: the read buffer, and an exception.
|
|
451
|
+
# The read buffer will be nil when EOF has been reached in which case
|
|
452
|
+
# the IO will be closed and the source removed from the loop.
|
|
453
|
+
# Returns the source so you can cancel the read with source.close!
|
|
454
|
+
def add_read(io, &block)
|
|
455
|
+
source = IOSource.new(io, :read)
|
|
456
|
+
source.trigger {
|
|
457
|
+
begin
|
|
458
|
+
loop do
|
|
459
|
+
buf = io.read_nonblock(4096*4) # 4 pages
|
|
460
|
+
block.call(buf, nil)
|
|
461
|
+
end
|
|
462
|
+
rescue IO::WaitReadable
|
|
463
|
+
# All good, wait until we're writable again
|
|
464
|
+
next true
|
|
465
|
+
rescue EOFError
|
|
466
|
+
block.call(nil, nil)
|
|
467
|
+
next false
|
|
468
|
+
rescue => e
|
|
469
|
+
block.call(nil, e)
|
|
470
|
+
next false
|
|
471
|
+
end
|
|
472
|
+
}
|
|
473
|
+
add_source(source)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Schedule a block of code to be run inside a Ruby Fiber.
|
|
477
|
+
# If the block calls loop.yield without any argument the fiber
|
|
478
|
+
# will simply be resumed repeatedly in subsequent iterations of
|
|
479
|
+
# the loop, until it terminates.
|
|
480
|
+
# If loop.yield is called with a block it signals that the proc should be
|
|
481
|
+
# executed as an async task and the result of the task delivered as return
|
|
482
|
+
# value from loop.yield. The block supplied must take a single argument
|
|
483
|
+
# which is a FiberTask instance. When the task is complete you *must* call
|
|
484
|
+
# task.done to return to the yielded fiber.
|
|
485
|
+
# The (optional) argument you supply to task.done(result) will be passed back
|
|
486
|
+
# to the yielded fiber.
|
|
487
|
+
#
|
|
488
|
+
# Example:
|
|
489
|
+
#
|
|
490
|
+
# loop.add_fiber {
|
|
491
|
+
# puts 'Waiting for slow result...'
|
|
492
|
+
# slow_result = loop.yield { |task|
|
|
493
|
+
# Thread.new { sleep 10; task.done('This took 10s') }
|
|
494
|
+
# }
|
|
495
|
+
# puts slow_result
|
|
496
|
+
# }
|
|
497
|
+
#
|
|
498
|
+
# Note: You can call this method from any thread. Since Ruby Fibers must
|
|
499
|
+
# be created from the same thread that runs them, EventCore will ensure
|
|
500
|
+
# the the fiber is created on the same thread as the main loop is running.
|
|
501
|
+
def add_fiber(&block)
|
|
502
|
+
# Fibers must be created on the same thread that resumes them.
|
|
503
|
+
# So if we're not on the main loop thread we get on it before
|
|
504
|
+
# creating the fiber
|
|
505
|
+
if Thread.current == @thread
|
|
506
|
+
source = FiberSource.new(self, block)
|
|
507
|
+
add_source(source)
|
|
508
|
+
else
|
|
509
|
+
source = FiberSource.new(self)
|
|
510
|
+
add_once {
|
|
511
|
+
source.create_fiber(block)
|
|
512
|
+
add_source(source)
|
|
513
|
+
}
|
|
514
|
+
source
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Must only be called from inside a fiber added with loop.add_fiber.
|
|
519
|
+
# Without arguments simply passes control back to the mainloop and resumes
|
|
520
|
+
# execution in next mainloop iteration. If passed a block, the block must
|
|
521
|
+
# take exactly one argument, which is a FiberTask. The block will be
|
|
522
|
+
# executed and the fiber scheduled for resumption when task.done is called.
|
|
523
|
+
# If an argument is passed to task.done then this will become the return value
|
|
524
|
+
# of the yield.
|
|
525
|
+
def yield(&block)
|
|
526
|
+
raise "Blocks passed to loop.yield must have arity 1" unless block.nil? or block.arity == 1
|
|
527
|
+
Fiber.yield block
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Must only be called from inside a fiber added with loop.add_fiber.
|
|
531
|
+
# Convenience function on top of loop.yield, returning the result of a block
|
|
532
|
+
# run in a new thread. Unlike loop.yield the block must not take any arguments;
|
|
533
|
+
# it is simply the raw result from the block that is send back to the yielding fiber.
|
|
534
|
+
def yield_from_thread(&block)
|
|
535
|
+
raise 'A block must be provided' if block.nil?
|
|
536
|
+
raise "Block must take exactly 0 arguments: #{block.arity}" unless block.arity == 0
|
|
537
|
+
|
|
538
|
+
self.yield do |task|
|
|
539
|
+
thread = Thread.new {
|
|
540
|
+
begin
|
|
541
|
+
result = block.call
|
|
542
|
+
ensure
|
|
543
|
+
add_once {
|
|
544
|
+
task.done(result)
|
|
545
|
+
thread.join
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
}
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Add a callback to invoke when the loop is quitting, before it becomes invalid.
|
|
553
|
+
# Sources added during the callback will not be invoked, but will be cleaned up.
|
|
554
|
+
def add_quit(&block)
|
|
555
|
+
@monitor.synchronize {
|
|
556
|
+
@quit_handlers << block
|
|
557
|
+
}
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Like Process.spawn(), invoking the given block in the main loop when
|
|
561
|
+
# the process child process exits. The block is called with the Process::Status
|
|
562
|
+
# object of the child.
|
|
563
|
+
#
|
|
564
|
+
# WARNING: The main loop install a SIGCHLD handler to automatically wait() on processes
|
|
565
|
+
# started this way. So this function will not work correctly if you tamper with
|
|
566
|
+
# SIGCHLD yourself.
|
|
567
|
+
#
|
|
568
|
+
# When you quit the loop any non-waited for children will be detached with Process.detach()
|
|
569
|
+
# to prevent zombies.
|
|
570
|
+
#
|
|
571
|
+
# Returns the PID of the child (that you should /not/ wait() on).
|
|
572
|
+
def spawn(*args, &block)
|
|
573
|
+
if @sigchld_source.nil?
|
|
574
|
+
@sigchld_source = add_unix_signal("CHLD") {
|
|
575
|
+
reap_children
|
|
576
|
+
}
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
pid = Process.spawn(*args)
|
|
580
|
+
@children << {:pid => pid, :block => block}
|
|
581
|
+
pid
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# The Thread instance currently iterating the run() method.
|
|
585
|
+
# nil if the loop is not running
|
|
586
|
+
def thread
|
|
587
|
+
@thread
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Returns true iff a thread is currently iterating the loop with the run() method.
|
|
591
|
+
def running?
|
|
592
|
+
!@thread.nil?
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Safe and clean shutdown of the loop.
|
|
596
|
+
# Note that the loop will only shut down on next iteration, not immediately.
|
|
597
|
+
def quit
|
|
598
|
+
# Does not require locking. If any data comes through in what ever form,
|
|
599
|
+
# we quit the loop
|
|
600
|
+
send_control('q')
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Start the loop, and do not return before some calls quit().
|
|
604
|
+
# When the loop returns (via quit) it will call close! on all sources.
|
|
605
|
+
def run
|
|
606
|
+
@thread = Thread.current
|
|
607
|
+
|
|
608
|
+
loop do
|
|
609
|
+
step
|
|
610
|
+
break if @do_quit
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
@monitor.synchronize {
|
|
614
|
+
@quit_handlers.each { |block| block.call }
|
|
615
|
+
|
|
616
|
+
@children.each { |child| Process.detach(child[:pid]) }
|
|
617
|
+
@children = nil
|
|
618
|
+
|
|
619
|
+
@sources.each { |source| source.close! }
|
|
620
|
+
@sources = nil
|
|
621
|
+
|
|
622
|
+
@control_source.close!
|
|
623
|
+
|
|
624
|
+
@thread = nil
|
|
625
|
+
}
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Expert: Run a single iteration of the main loop.
|
|
629
|
+
def step
|
|
630
|
+
# Collect sources
|
|
631
|
+
ready_sources = []
|
|
632
|
+
select_sources_by_ios = {}
|
|
633
|
+
read_ios = []
|
|
634
|
+
write_ios = []
|
|
635
|
+
timeout = nil
|
|
636
|
+
|
|
637
|
+
@monitor.synchronize {
|
|
638
|
+
@sources.delete_if do |source|
|
|
639
|
+
if source.closed?
|
|
640
|
+
true
|
|
641
|
+
else
|
|
642
|
+
source_ready = source.ready?
|
|
643
|
+
ready_sources << source if source_ready
|
|
644
|
+
|
|
645
|
+
io = source.select_io
|
|
646
|
+
unless io.nil? || io.closed?
|
|
647
|
+
case source.select_type
|
|
648
|
+
when :read
|
|
649
|
+
read_ios << io
|
|
650
|
+
when :write
|
|
651
|
+
write_ios << io
|
|
652
|
+
else
|
|
653
|
+
raise "Invalid source select_type: #{source.select_type}"
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
select_sources_by_ios[io] = source
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
dt = source_ready ? 0 : source.timeout
|
|
660
|
+
timeout = timeout.nil? ?
|
|
661
|
+
dt : (dt.nil? ? timeout : (timeout < dt ? timeout : dt))
|
|
662
|
+
|
|
663
|
+
false
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
# Release lock while we're selecting so users can add sources. add_source() will see
|
|
669
|
+
# that we are stuck in a select() and do send_wakeup().
|
|
670
|
+
# Note: Only select() without locking, everything else must be locked!
|
|
671
|
+
read_ios, write_ios, exception_ios = IO.select(read_ios, write_ios, [], timeout)
|
|
672
|
+
|
|
673
|
+
@monitor.synchronize {
|
|
674
|
+
# On timeout read_ios will be nil
|
|
675
|
+
unless read_ios.nil?
|
|
676
|
+
read_ios.each { |io|
|
|
677
|
+
ready_sources << select_sources_by_ios[io]
|
|
678
|
+
}
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
unless write_ios.nil?
|
|
682
|
+
write_ios.each { |io|
|
|
683
|
+
ready_sources << select_sources_by_ios[io]
|
|
684
|
+
}
|
|
685
|
+
end
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
# Dispatch all sources marked ready
|
|
689
|
+
ready_sources.each { |source|
|
|
690
|
+
source.notify_trigger
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@do_quit = true if @control_source.closed?
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Expert: wake up the main loop, forcing it to check all sources.
|
|
697
|
+
# Useful if you're twiddling readyness of sources "out of band".
|
|
698
|
+
def send_wakeup
|
|
699
|
+
send_control('.')
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
private
|
|
703
|
+
def send_control(char)
|
|
704
|
+
raise "Illegal control character '#{char}'" unless ['.', 'q'].include?(char)
|
|
705
|
+
@control_source.write(char)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
private
|
|
709
|
+
def reap_children
|
|
710
|
+
# Waiting on pid -1, to reap any child would be tempting, but that could conflict
|
|
711
|
+
# with other parts of code, not using EventCore, trying to wait() on those pids.
|
|
712
|
+
# In stead we have to check each child explicitly spawned via loop.spawn(). This
|
|
713
|
+
# is O(N) in the number of children, naturally, but I haven't found a better way
|
|
714
|
+
# that is robust.
|
|
715
|
+
@children.delete_if {|child|
|
|
716
|
+
if Process.wait(child[:pid], Process::WNOHANG)
|
|
717
|
+
status = $?
|
|
718
|
+
child[:block].call(status) unless child[:block].nil?
|
|
719
|
+
true
|
|
720
|
+
else
|
|
721
|
+
false
|
|
722
|
+
end
|
|
723
|
+
}
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: event_core
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mikkel Kamstrup Erlandsen
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-03-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A versatile event loop for Ruby with rich possibilities for integration
|
|
14
|
+
email: mikkel.kamstrup@xamarin.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/event_core.rb
|
|
20
|
+
homepage: http://github.com/kamstrup/event_core
|
|
21
|
+
licenses:
|
|
22
|
+
- MIT
|
|
23
|
+
metadata: {}
|
|
24
|
+
post_install_message:
|
|
25
|
+
rdoc_options: []
|
|
26
|
+
require_paths:
|
|
27
|
+
- lib
|
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '>='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
requirements: []
|
|
39
|
+
rubyforge_project:
|
|
40
|
+
rubygems_version: 2.0.2
|
|
41
|
+
signing_key:
|
|
42
|
+
specification_version: 4
|
|
43
|
+
summary: Event Core
|
|
44
|
+
test_files: []
|