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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/event_core.rb +727 -0
  3. metadata +44 -0
@@ -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
@@ -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: []