event_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []