bran 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +43 -0
- data/ext/libuv/Rakefile +93 -0
- data/lib/bran.rb +4 -0
- data/lib/bran/ext.rb +35 -0
- data/lib/bran/ext/io.rb +107 -0
- data/lib/bran/ext/rainbows.rb +9 -0
- data/lib/bran/ext/rainbows/bran.rb +64 -0
- data/lib/bran/ext/tcp_server.rb +17 -0
- data/lib/bran/fiber_manager.rb +43 -0
- data/lib/bran/libuv.rb +5 -0
- data/lib/bran/libuv/ffi.rb +103 -0
- data/lib/bran/libuv/reactor.rb +361 -0
- data/lib/bran/libuv/util.rb +19 -0
- metadata +162 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 36a615f86cbc15a2b7c138816df60678ca2a0786
|
4
|
+
data.tar.gz: 48570f692b614243db475256f19265f74993be70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c14a66422d3310bc2619afb7c59e883c6c9ec6022b7e9139b7800b2a5aa3ca1114cab48ce76d4bce553ffbcab647de60c5604dfbc8f4f6159649ea3a18c31e6e
|
7
|
+
data.tar.gz: 2ba3d09c4a6786b300e3c4c96030ed692fdc60a880c539a396efc42ba081eb66c190529e4c9bc217d12fd415399b1bacc1088b9fe5b0261f6f616fcb17ab21e8
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Bran
|
2
|
+
|
3
|
+
## Integrations
|
4
|
+
|
5
|
+
Bran is most useful when integrated with every part of your application that performs I/O. Because I/O is commonly abstracted away from our applications by libraries, Bran includes integrations and patches for various third-party libraries so that they can use Bran to yield control to other Fibers while waiting for I/O.
|
6
|
+
|
7
|
+
It's important to realize that some of these integrations may depend on "implementation details" of the libraries that they patch. Some libraries do not expose the public interfaces we need to integrate Bran with their I/O path, and some libraries do not even make it _clear_ which interfaces are public API and which interfaces are implementation details.
|
8
|
+
|
9
|
+
Bran integrations try to use only public interfaces wherever possible, and will otherwise try to make explicit assumptions about the interfaces they expect. Some of these assumptions can be checked at runtime when loading the integrations (attempt to "fail eagerly" in the case of poor integration). To enable this behavior, do the following before loading any Bran integration:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require "bran/ext"
|
13
|
+
|
14
|
+
Bran::Ext.check_assumptions = true
|
15
|
+
|
16
|
+
# Load specific Bran integrations here.
|
17
|
+
```
|
18
|
+
|
19
|
+
The description of each Bran integration will also be labeled "Tested with" for known good combinations of dependent gems.
|
20
|
+
|
21
|
+
### Rainbows
|
22
|
+
|
23
|
+
Use Bran with [Rainbows][rainbows], a [Unicorn][unicorn]-based and [Unicorn][unicorn]-compatible webserver from the creators of [Unicorn][unicorn].
|
24
|
+
|
25
|
+
Each worker process will allow multiple concurrent connections, accepted by the Bran adapter into a fixed-size fiber pool, with the fibers being managed by Bran. Without any other Bran integrations, you can expect this configuration to only process one request at a time, with performance on par with that of Unicorn. Using other Bran I/O integrations while handling requests will give the fibers a chance to work concurrently, increasing performance for I/O-bound loads.
|
26
|
+
|
27
|
+
To activate the Bran integration, try adding the following example configuration to your Unicorn/Rainbows config file (often called `unicorn.rb`):
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "bran/ext/rainbows"
|
31
|
+
|
32
|
+
Rainbows! do
|
33
|
+
use :Bran # use the Bran adapter as the concurrency manager
|
34
|
+
worker_connections 100 # accept 100 connections per worker (100 fibers)
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Tested with:
|
39
|
+
- `rainbows 5.0.0` (`unicorn 5.0.0`, `kgio 2.10.0`)
|
40
|
+
- `rainbows 5.0.0` (`unicorn 5.0.1`, `kgio 2.10.0`)
|
41
|
+
|
42
|
+
[rainbows]: http://rainbows.bogomips.org/
|
43
|
+
[unicorn]: http://unicorn.bogomips.org/
|
data/ext/libuv/Rakefile
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
require "rake/clean"
|
3
|
+
require "ffi"
|
4
|
+
|
5
|
+
FILES = {}
|
6
|
+
|
7
|
+
task :default => [:build, :compact]
|
8
|
+
|
9
|
+
def self.file_task(filename, opts, &block)
|
10
|
+
name, dep = opts.is_a?(Hash) ? opts.to_a.first : [opts, nil]
|
11
|
+
|
12
|
+
FILES[name] = filename
|
13
|
+
CLEAN.include filename
|
14
|
+
task name => filename
|
15
|
+
|
16
|
+
if dep
|
17
|
+
file filename => FILES[dep], &block
|
18
|
+
else
|
19
|
+
file filename, &block
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def cmd(string)
|
24
|
+
fail "Command failed: #{string}" unless system(string)
|
25
|
+
end
|
26
|
+
|
27
|
+
file_task "libuv.tar.gz", :download_tarball do
|
28
|
+
version = "1.8.0"
|
29
|
+
release = "https://github.com/libuv/libuv/archive/v#{version}.tar.gz"
|
30
|
+
cmd "wget -O #{FILES[:download_tarball]} #{release}"
|
31
|
+
end
|
32
|
+
|
33
|
+
file_task "libuv", :download => :download_tarball do
|
34
|
+
cmd "tar -zxf #{FILES[:download_tarball]}"
|
35
|
+
cmd "mv libuv-* #{FILES[:download]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
file_task "autogen.touch", :autogen => :download do
|
39
|
+
cmd "/usr/bin/env sh -c 'cd #{FILES[:download]} && ./autogen.sh'"
|
40
|
+
cmd "touch #{FILES[:autogen]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
file_task "configure.touch", :configure => :autogen do
|
44
|
+
cmd "/usr/bin/env sh -c 'cd #{FILES[:download]} && ./configure'"
|
45
|
+
cmd "touch #{FILES[:configure]}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO: try to get patch accepted upstream?
|
49
|
+
file_task "patch.touch", :patch => :configure do
|
50
|
+
uv_header = "#{FILES[:download]}/include/uv.h"
|
51
|
+
uv_source = "#{FILES[:download]}/src/uv-common.c"
|
52
|
+
|
53
|
+
##
|
54
|
+
# Patch 1 - runtime detection for the sizeof each struct type.
|
55
|
+
|
56
|
+
types = []
|
57
|
+
not_types = []
|
58
|
+
File.read(uv_header).each_line do |line|
|
59
|
+
# Record the type from each struct typedef.
|
60
|
+
match = /typedef struct uv_\w+_s (uv_\w+?)_t;/.match line
|
61
|
+
types << match[1] if match
|
62
|
+
|
63
|
+
# Don't record the type if it already has a sizeof function.
|
64
|
+
match = /UV_EXTERN size_t (uv_\w+?)_sizeof\(void\);/.match line
|
65
|
+
not_types << match[1] if match
|
66
|
+
end
|
67
|
+
types -= not_types
|
68
|
+
|
69
|
+
# Declare the sizeof function for each recorded type.
|
70
|
+
File.open uv_header, "a" do |file|
|
71
|
+
types.each do |type|
|
72
|
+
file.puts("UV_EXTERN size_t #{type}_sizeof(void);")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Implement the sizeof function for each recorded type.
|
77
|
+
File.open uv_source, "a" do |file|
|
78
|
+
types.each do |type|
|
79
|
+
file.puts("size_t #{type}_sizeof(void) { return sizeof(#{type}_t); }")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
file_task "libuv.#{::FFI::Platform::LIBSUFFIX}", :build => :patch do
|
85
|
+
cmd "/usr/bin/env sh -c 'cd #{FILES[:download]} && make'"
|
86
|
+
cmd "cp #{FILES[:download]}/.libs/#{FILES[:build]} ."
|
87
|
+
end
|
88
|
+
|
89
|
+
task :compact => FILES[:build] do
|
90
|
+
FILES.each do |key, filename|
|
91
|
+
cmd "rm -rf #{filename}" unless key == :build
|
92
|
+
end
|
93
|
+
end
|
data/lib/bran.rb
ADDED
data/lib/bran/ext.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
module Bran
|
3
|
+
class Ext
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :check_assumptions
|
7
|
+
|
8
|
+
def assume(&block)
|
9
|
+
return unless @check_assumptions
|
10
|
+
|
11
|
+
instance_eval &block
|
12
|
+
end
|
13
|
+
|
14
|
+
def check(cond)
|
15
|
+
return if cond
|
16
|
+
|
17
|
+
match = caller.first.match(/\A(.+?):(\d+):in/)
|
18
|
+
line = File.read(match[1]).each_line.to_a[Integer(match[2]) - 1].strip
|
19
|
+
|
20
|
+
fail "Bran extension compatibility check failed: #{line}"
|
21
|
+
end
|
22
|
+
|
23
|
+
REGISTRY = {}
|
24
|
+
|
25
|
+
def []=(ext_name, value)
|
26
|
+
REGISTRY[ext_name] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](ext_name)
|
30
|
+
REGISTRY[ext_name]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/bran/ext/io.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
|
2
|
+
require_relative "../../bran"
|
3
|
+
require_relative "../../bran/ext"
|
4
|
+
|
5
|
+
::Bran::Ext[:io] = true
|
6
|
+
|
7
|
+
# TODO: split to io-read.rb
|
8
|
+
Module.new do
|
9
|
+
IO.prepend self
|
10
|
+
|
11
|
+
def getbyte(*)
|
12
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
13
|
+
fm.wait_for_readable!(to_i) if fm
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def getc(*)
|
18
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
19
|
+
fm.wait_for_readable!(to_i) if fm
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def gets(*)
|
24
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
25
|
+
fm.wait_for_readable!(to_i) if fm
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(*)
|
30
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
31
|
+
fm.wait_for_readable!(to_i) if fm
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def readbyte(*)
|
36
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
37
|
+
fm.wait_for_readable!(to_i) if fm
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def readchar(*)
|
42
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
43
|
+
fm.wait_for_readable!(to_i) if fm
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def readlines(*)
|
48
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
49
|
+
fm.wait_for_readable!(to_i) if fm
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def readpartial(*)
|
54
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
55
|
+
fm.wait_for_readable!(to_i) if fm
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
def sysread(*)
|
60
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
61
|
+
fm.wait_for_readable!(to_i) if fm
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# TODO: split to io-select.rb
|
67
|
+
Module.new do
|
68
|
+
IO.singleton_class.prepend self
|
69
|
+
|
70
|
+
def select(r_ary = nil, w_ary = nil, e_ary = nil, timeout = nil)
|
71
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
72
|
+
return super unless fm
|
73
|
+
|
74
|
+
raise NotImplementedError if e_ary && e_ary.any? # TODO: support e_ary?
|
75
|
+
|
76
|
+
# TODO: move inner implementation to inside FiberManager?
|
77
|
+
fiber = Fiber.current
|
78
|
+
timer = nil
|
79
|
+
finish = Proc.new do |item|
|
80
|
+
begin
|
81
|
+
r_ary.each { |io| fm.loop.pop_readable(Integer(io)) } if r_ary
|
82
|
+
w_ary.each { |io| fm.loop.pop_writable(Integer(io)) } if w_ary
|
83
|
+
fm.loop.timer_cancel(timer) if timer
|
84
|
+
ensure
|
85
|
+
fiber.resume(item)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
r_ary.each do |io|
|
90
|
+
fm.loop.push_readable(Integer(io), Proc.new { |*|
|
91
|
+
finish.call([[io], [], []])
|
92
|
+
})
|
93
|
+
end if r_ary
|
94
|
+
|
95
|
+
w_ary.each do |io|
|
96
|
+
fm.loop.push_writable(Integer(io), Proc.new { |*|
|
97
|
+
finish.call([[], [io], []])
|
98
|
+
})
|
99
|
+
end if w_ary
|
100
|
+
|
101
|
+
if timeout
|
102
|
+
timer = fm.loop.timer_oneshot(timeout) { timer = nil; finish.call(nil) }
|
103
|
+
end
|
104
|
+
|
105
|
+
Fiber.yield
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
::Bran::Ext.assume do
|
3
|
+
::Rainbows::HttpServer.instance_method(:worker_connections).tap do |m|
|
4
|
+
check m.owner == ::Rainbows::HttpServer
|
5
|
+
check m.arity == 0
|
6
|
+
end
|
7
|
+
|
8
|
+
::Rainbows::HttpServer.instance_method(:init_worker_process).tap do |m|
|
9
|
+
check m.owner == ::Unicorn::HttpServer
|
10
|
+
check m.arity == 1
|
11
|
+
end
|
12
|
+
|
13
|
+
::Rainbows::HttpServer.instance_method(:reopen_worker_logs).tap do |m|
|
14
|
+
check m.owner == ::Unicorn::HttpServer
|
15
|
+
check m.arity == 1
|
16
|
+
end
|
17
|
+
|
18
|
+
::Unicorn::HttpServer.instance_method(:process_client).tap do |m|
|
19
|
+
check m.owner == ::Unicorn::HttpServer
|
20
|
+
check m.arity == 1
|
21
|
+
end
|
22
|
+
|
23
|
+
check ::Unicorn::Worker.instance_method(:nr).arity == 0
|
24
|
+
check ::Kgio::TCPServer.instance_method(:to_i).arity == 0
|
25
|
+
end
|
26
|
+
|
27
|
+
module Rainbows
|
28
|
+
module Bran
|
29
|
+
include ::Rainbows::Base
|
30
|
+
|
31
|
+
def worker_loop(worker)
|
32
|
+
readers = init_worker_process(worker)
|
33
|
+
|
34
|
+
# We have to skip the implementation from Rainbows::Base,
|
35
|
+
# and use the implementation from Unicorn::HttpServer directly.
|
36
|
+
process_client = ::Unicorn::HttpServer.instance_method(:process_client)
|
37
|
+
process_client = process_client.bind(self)
|
38
|
+
|
39
|
+
manager = ::Bran::FiberManager.new
|
40
|
+
stopping = false
|
41
|
+
|
42
|
+
manager.run! do
|
43
|
+
manager.loop.signal_start(:INT) { exit!(0) }
|
44
|
+
manager.loop.signal_start(:TERM) { exit!(0) }
|
45
|
+
manager.loop.signal_start(:USR1) { reopen_worker_logs(worker.nr) } # TODO: test
|
46
|
+
manager.loop.signal_start(:QUIT) { stopping = true } # TODO: test softness
|
47
|
+
|
48
|
+
readers.each do |reader|
|
49
|
+
next unless reader.is_a?(::Kgio::TCPServer) # TODO: other readers?
|
50
|
+
|
51
|
+
worker_connections.times.map do |i|
|
52
|
+
::Fiber.new do
|
53
|
+
until stopping # TODO: rescue and report errors here
|
54
|
+
manager.wait_for_readable!(reader)
|
55
|
+
process_client.call(reader.kgio_accept)
|
56
|
+
end
|
57
|
+
end.resume
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require_relative "../../bran"
|
3
|
+
require_relative "../../bran/ext"
|
4
|
+
|
5
|
+
::Bran::Ext[:tcp_server] = true
|
6
|
+
|
7
|
+
require "socket"
|
8
|
+
|
9
|
+
Module.new do
|
10
|
+
TCPServer.prepend self
|
11
|
+
|
12
|
+
def accept(*)
|
13
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
14
|
+
fm.wait_for_readable!(to_i) if fm
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
require "fiber"
|
3
|
+
|
4
|
+
module Bran
|
5
|
+
class FiberManager
|
6
|
+
attr_reader :loop # TODO: hide loop when rest of interface is stable
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@loop = LibUV::Reactor.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def run!
|
13
|
+
Thread.current.thread_variable_set(:fiber_manager, self)
|
14
|
+
|
15
|
+
yield if block_given?
|
16
|
+
|
17
|
+
@loop.run!
|
18
|
+
ensure
|
19
|
+
Thread.current.thread_variable_set(:fiber_manager, nil)
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop!
|
23
|
+
Thread.current.thread_variable_set(:fiber_manager, nil)
|
24
|
+
@loop.stop!
|
25
|
+
end
|
26
|
+
|
27
|
+
def wait_for_readable!(fd)
|
28
|
+
@loop.push_readable(Integer(fd), ::Fiber.current, false)
|
29
|
+
::Fiber.yield
|
30
|
+
end
|
31
|
+
|
32
|
+
def wait_for_writable!(fd)
|
33
|
+
@loop.push_writable(Integer(fd), ::Fiber.current, true)
|
34
|
+
::Fiber.yield
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait_for_seconds!(seconds)
|
38
|
+
@loop.timer_oneshot_wake(seconds, ::Fiber.current)
|
39
|
+
::Fiber.yield
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/lib/bran/libuv.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
require "ffi"
|
3
|
+
|
4
|
+
module Bran
|
5
|
+
module LibUV
|
6
|
+
module FFI
|
7
|
+
extend ::FFI::Library
|
8
|
+
|
9
|
+
libfile = "libuv.#{::FFI::Platform::LIBSUFFIX}"
|
10
|
+
|
11
|
+
ffi_lib \
|
12
|
+
File.expand_path("../../../ext/libuv/#{libfile}", File.dirname(__FILE__))
|
13
|
+
|
14
|
+
opts = {
|
15
|
+
blocking: true # only necessary on MRI to deal with the GIL.
|
16
|
+
}
|
17
|
+
|
18
|
+
UV_READABLE = 1
|
19
|
+
UV_WRITABLE = 2
|
20
|
+
|
21
|
+
# Struct sizes/allocators
|
22
|
+
%w(uv_loop uv_poll uv_signal uv_timer).each do |type|
|
23
|
+
eval <<-RUBY
|
24
|
+
typedef :pointer, :#{type}_ptr
|
25
|
+
attach_function :#{type}_sizeof, [], :size_t, **opts
|
26
|
+
#{type.upcase}_SIZEOF = #{type}_sizeof
|
27
|
+
|
28
|
+
def self.#{type}_alloc
|
29
|
+
ptr = ::FFI::MemoryPointer.new(#{type.upcase}_SIZEOF)
|
30
|
+
ptr.autorelease = false
|
31
|
+
ptr
|
32
|
+
end
|
33
|
+
RUBY
|
34
|
+
end
|
35
|
+
|
36
|
+
typedef :int, :uv_os_sock_t # not true on Windows, but we don't care
|
37
|
+
|
38
|
+
typedef enum([
|
39
|
+
:default,
|
40
|
+
:once,
|
41
|
+
:nowait
|
42
|
+
]), :uv_run_mode
|
43
|
+
|
44
|
+
##
|
45
|
+
# Callback factory methods.
|
46
|
+
#
|
47
|
+
# WARNING: If your Ruby code doesn't retain a reference to the
|
48
|
+
# FFI::Function object after passing it to a C function call,
|
49
|
+
# it may be garbage collected while C still holds the pointer,
|
50
|
+
# potentially resulting in a segmentation fault.
|
51
|
+
|
52
|
+
typedef :pointer, :uv_poll_cb_ptr
|
53
|
+
def self.uv_poll_cb(&block)
|
54
|
+
# (handle, status, events)
|
55
|
+
params = [:pointer, :int, :int]
|
56
|
+
::FFI::Function.new :void, params, blocking: true do |*args|
|
57
|
+
block.call(*args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
typedef :pointer, :uv_signal_cb_ptr
|
62
|
+
def self.uv_signal_cb(&block)
|
63
|
+
# (handle, signo)
|
64
|
+
params = [:pointer, :int]
|
65
|
+
::FFI::Function.new :void, params, blocking: true do |*args|
|
66
|
+
block.call(*args)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
typedef :pointer, :uv_timer_cb_ptr
|
71
|
+
def self.uv_timer_cb(&block)
|
72
|
+
# (handle)
|
73
|
+
params = [:pointer]
|
74
|
+
::FFI::Function.new :void, params, blocking: true do |*args|
|
75
|
+
block.call(*args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
attach_function :uv_strerror, [:int], :string, **opts
|
80
|
+
attach_function :uv_err_name, [:int], :string, **opts
|
81
|
+
|
82
|
+
attach_function :uv_loop_init, [:uv_loop_ptr], :int, **opts
|
83
|
+
attach_function :uv_loop_close, [:uv_loop_ptr], :int, **opts
|
84
|
+
attach_function :uv_loop_alive, [:uv_loop_ptr], :int, **opts
|
85
|
+
|
86
|
+
attach_function :uv_run, [:uv_loop_ptr, :uv_run_mode], :int, **opts
|
87
|
+
attach_function :uv_stop, [:uv_loop_ptr], :void, **opts
|
88
|
+
|
89
|
+
attach_function :uv_poll_init, [:uv_loop_ptr, :uv_poll_ptr, :int], :int, **opts
|
90
|
+
attach_function :uv_poll_init_socket, [:uv_loop_ptr, :uv_poll_ptr, :uv_os_sock_t], :int, **opts
|
91
|
+
attach_function :uv_poll_start, [:uv_poll_ptr, :int, :uv_poll_cb_ptr], :int, **opts
|
92
|
+
attach_function :uv_poll_stop, [:uv_poll_ptr], :int, **opts
|
93
|
+
|
94
|
+
attach_function :uv_signal_init, [:uv_loop_ptr, :uv_signal_ptr], :int, **opts
|
95
|
+
attach_function :uv_signal_start, [:uv_signal_ptr, :uv_signal_cb_ptr, :int], :int, **opts
|
96
|
+
attach_function :uv_signal_stop, [:uv_signal_ptr], :int, **opts
|
97
|
+
|
98
|
+
attach_function :uv_timer_init, [:uv_loop_ptr, :uv_timer_ptr], :int, **opts
|
99
|
+
attach_function :uv_timer_start, [:uv_timer_ptr, :uv_timer_cb_ptr, :uint64, :uint64], :int, **opts
|
100
|
+
attach_function :uv_timer_stop, [:uv_timer_ptr], :int, **opts
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,361 @@
|
|
1
|
+
|
2
|
+
module Bran
|
3
|
+
module LibUV
|
4
|
+
class Reactor
|
5
|
+
|
6
|
+
# Raised when an operation is performed on an already-destroyed Loop.
|
7
|
+
class DestroyedError < RuntimeError; end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@ptr = FFI.uv_loop_alloc
|
11
|
+
Util.error_check "creating the loop",
|
12
|
+
FFI.uv_loop_init(@ptr)
|
13
|
+
|
14
|
+
@finalizer = self.class.create_finalizer_for(@ptr)
|
15
|
+
ObjectSpace.define_finalizer(self, @finalizer)
|
16
|
+
|
17
|
+
@available_polls = []
|
18
|
+
@running_reads = {}
|
19
|
+
@running_writes = {}
|
20
|
+
@fds_by_read_addr = {}
|
21
|
+
@fds_by_write_addr = {}
|
22
|
+
@on_readables = {}
|
23
|
+
@on_writables = {}
|
24
|
+
|
25
|
+
@available_signals = []
|
26
|
+
@running_signals = {}
|
27
|
+
|
28
|
+
@available_timers = []
|
29
|
+
@running_timers = {}
|
30
|
+
|
31
|
+
@poll_read_callback = FFI.uv_poll_cb(&method(:_poll_read_callback))
|
32
|
+
@poll_write_callback = FFI.uv_poll_cb(&method(:_poll_write_callback))
|
33
|
+
|
34
|
+
# TODO: add more Ruby-compatible signal handlers by default?
|
35
|
+
signal_start(:INT) { @abort_signal = :INT; stop! }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Free the native resources associated with this object. This will
|
39
|
+
# be done automatically on garbage collection if not called explicitly.
|
40
|
+
def destroy
|
41
|
+
if @finalizer
|
42
|
+
@finalizer.call
|
43
|
+
ObjectSpace.undefine_finalizer(self)
|
44
|
+
end
|
45
|
+
@ptr = @finalizer = nil
|
46
|
+
|
47
|
+
@available_polls = \
|
48
|
+
@running_reads = @running_writes = \
|
49
|
+
@fds_by_read_addr = @fds_by_write_addr = \
|
50
|
+
@on_readables = @on_writables = \
|
51
|
+
@available_signals = @running_signals = \
|
52
|
+
@available_timers = @running_timers = \
|
53
|
+
@poll_read_callback = @poll_write_callback = nil
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
def ptr
|
60
|
+
raise DestroyedError unless @ptr
|
61
|
+
@ptr
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def self.create_finalizer_for(ptr)
|
66
|
+
Proc.new do
|
67
|
+
FFI.uv_loop_close(ptr)
|
68
|
+
# TODO: prevent running finalizer when loop hasn't been stopped?
|
69
|
+
ptr.free
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Capture exceptions raised from callbacks, stopping the loop,
|
74
|
+
# capturing the exception to be re-raised outside the loop in #run!.
|
75
|
+
# @api private
|
76
|
+
def rescue_abort
|
77
|
+
yield
|
78
|
+
rescue Exception => ex
|
79
|
+
@abort_exception = ex
|
80
|
+
stop!
|
81
|
+
end
|
82
|
+
|
83
|
+
# Run the libuv event loop in default (blocking) mode,
|
84
|
+
# running until stopped or until all handles are removed.
|
85
|
+
def run!
|
86
|
+
@abort_exception = nil
|
87
|
+
@abort_signal = nil
|
88
|
+
|
89
|
+
rc = FFI.uv_run(ptr, :default)
|
90
|
+
|
91
|
+
# If an exception or signal caused the stop, re-raise it here.
|
92
|
+
raise @abort_exception if @abort_exception
|
93
|
+
Process.kill(@abort_signal, Process.pid) if @abort_signal
|
94
|
+
|
95
|
+
Util.error_check "running the loop in blocking mode", rc
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return true if there are active handles or request in the loop.
|
99
|
+
def stop!
|
100
|
+
FFI.uv_stop(ptr)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Push the given handler for the given fd, adding if necessary.
|
104
|
+
# If persistent is false, the handler will be popped after one trigger.
|
105
|
+
def push_readable(fd, handler, persistent = true)
|
106
|
+
ptr = ptr()
|
107
|
+
fd = Integer(fd)
|
108
|
+
|
109
|
+
# TODO: worry about readable vs writable mixing/overlap
|
110
|
+
if (readables = @on_readables[fd])
|
111
|
+
readables << [handler, persistent]
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
poll = @available_polls.pop || FFI.uv_poll_alloc
|
116
|
+
@running_reads[fd] = poll
|
117
|
+
@fds_by_read_addr[poll.address] = fd
|
118
|
+
@on_readables[fd] = [[handler, persistent]]
|
119
|
+
|
120
|
+
# TODO: investigate if need not init existing available_polls
|
121
|
+
Util.error_check "creating the poll readable entry",
|
122
|
+
FFI.uv_poll_init(ptr, poll, fd)
|
123
|
+
|
124
|
+
Util.error_check "starting the poll readable entry",
|
125
|
+
FFI.uv_poll_start(poll, FFI::UV_READABLE, @poll_read_callback)
|
126
|
+
|
127
|
+
fd
|
128
|
+
end
|
129
|
+
|
130
|
+
# Push the given handler for the given fd, adding if necessary.
|
131
|
+
# If persistent is false, the handler will be popped after one trigger.
|
132
|
+
def push_writable(fd, handler, persistent = true)
|
133
|
+
ptr = ptr()
|
134
|
+
fd = Integer(fd)
|
135
|
+
|
136
|
+
# TODO: worry about writable vs writable mixing/overlap
|
137
|
+
if (writables = @on_writables[fd])
|
138
|
+
writables << [handler, persistent]
|
139
|
+
return
|
140
|
+
end
|
141
|
+
|
142
|
+
poll = @available_polls.pop || FFI.uv_poll_alloc
|
143
|
+
@running_writes[fd] = poll
|
144
|
+
@fds_by_write_addr[poll.address] = fd
|
145
|
+
@on_writables[fd] = [[handler, persistent]]
|
146
|
+
|
147
|
+
# TODO: investigate if need not init existing available_polls
|
148
|
+
Util.error_check "creating the poll writable entry",
|
149
|
+
FFI.uv_poll_init(ptr, poll, fd)
|
150
|
+
|
151
|
+
Util.error_check "starting the poll writable entry",
|
152
|
+
FFI.uv_poll_start(poll, FFI::UV_WRITABLE, @poll_write_callback)
|
153
|
+
|
154
|
+
fd
|
155
|
+
end
|
156
|
+
|
157
|
+
# Remove the next readable handler for the given fd.
|
158
|
+
def pop_readable(fd)
|
159
|
+
fd = Integer(fd)
|
160
|
+
|
161
|
+
readables = @on_readables[fd]
|
162
|
+
return unless readables
|
163
|
+
|
164
|
+
readables.pop
|
165
|
+
return unless readables.empty?
|
166
|
+
|
167
|
+
@on_readables.delete(fd)
|
168
|
+
poll = @running_reads.delete(fd)
|
169
|
+
@fds_by_read_addr.delete(poll.address)
|
170
|
+
|
171
|
+
Util.error_check "stopping the poll readable entry",
|
172
|
+
FFI.uv_poll_stop(poll)
|
173
|
+
|
174
|
+
@available_polls << poll
|
175
|
+
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
|
179
|
+
# Remove the next writable handler for the given fd.
|
180
|
+
def pop_writable(fd)
|
181
|
+
fd = Integer(fd)
|
182
|
+
|
183
|
+
writables = @on_writables[fd]
|
184
|
+
return unless writables
|
185
|
+
|
186
|
+
writables.pop
|
187
|
+
return unless writables.empty?
|
188
|
+
|
189
|
+
@on_writables.delete(fd)
|
190
|
+
poll = @running_writes.delete(fd)
|
191
|
+
@fds_by_write_addr.delete(poll.address)
|
192
|
+
|
193
|
+
Util.error_check "stopping the poll writable entry",
|
194
|
+
FFI.uv_poll_stop(poll)
|
195
|
+
|
196
|
+
@available_polls << poll
|
197
|
+
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
|
201
|
+
# Start handling the given signal, running the given block when it occurs.
|
202
|
+
def signal_start(signo, &block)
|
203
|
+
ptr = ptr()
|
204
|
+
signo = Signal.list.fetch(signo.to_s) unless signo.is_a?(Integer)
|
205
|
+
|
206
|
+
signal_stop(signo) if @running_signals.has_key?(signo)
|
207
|
+
|
208
|
+
callback = FFI.uv_signal_cb do |_, _|
|
209
|
+
rescue_abort do
|
210
|
+
block.call self, signo
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
signal = @available_signals.pop || FFI.uv_signal_alloc
|
215
|
+
@running_signals[signo] = [signal, callback]
|
216
|
+
|
217
|
+
# TODO: investigate if need not init existing available_signals
|
218
|
+
Util.error_check "creating the signal item",
|
219
|
+
FFI.uv_signal_init(ptr, signal)
|
220
|
+
|
221
|
+
Util.error_check "starting the signal item",
|
222
|
+
FFI.uv_signal_start(signal, callback, signo)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Stop handling the given signal.
|
226
|
+
def signal_stop(signo)
|
227
|
+
signo = Signal.list.fetch(signo.to_s) unless signo.is_a?(Integer)
|
228
|
+
|
229
|
+
signal, callback = @running_signals.delete(signo)
|
230
|
+
|
231
|
+
return unless signal
|
232
|
+
|
233
|
+
Util.error_check "stopping the signal item",
|
234
|
+
FFI.uv_signal_stop(signal)
|
235
|
+
|
236
|
+
@available_signals << signal
|
237
|
+
|
238
|
+
nil
|
239
|
+
end
|
240
|
+
|
241
|
+
# Start a timer to run the given block after the given timeout.
|
242
|
+
# If a repeat_interval is given, after the first run, the block will be
|
243
|
+
# run repeatedly at that interval. If a repeat_interval is not given,
|
244
|
+
# or given as nil or 0, timer_cancel is called automatically at first run.
|
245
|
+
# Both timeout and repeat_interval should be given in seconds.
|
246
|
+
def timer_start(timeout, repeat_interval = nil, &block)
|
247
|
+
ptr = ptr()
|
248
|
+
|
249
|
+
timeout = (timeout * 1000).ceil
|
250
|
+
|
251
|
+
repeat = false
|
252
|
+
if repeat_interval and repeat_interval > 0
|
253
|
+
repeat_interval = (repeat_interval * 1000).ceil
|
254
|
+
repeat = true
|
255
|
+
else
|
256
|
+
repeat_interval = 0
|
257
|
+
end
|
258
|
+
|
259
|
+
raise ArgumentError, "callback block required" unless block
|
260
|
+
|
261
|
+
timer = @available_timers.pop || FFI.uv_timer_alloc
|
262
|
+
id = timer.address
|
263
|
+
|
264
|
+
callback = FFI.uv_timer_cb do |_|
|
265
|
+
rescue_abort do
|
266
|
+
block.call self, id
|
267
|
+
|
268
|
+
timer_cancel(id) unless repeat
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
@running_timers[id] = [timer, callback]
|
273
|
+
|
274
|
+
# TODO: investigate if need not init existing available_timers
|
275
|
+
Util.error_check "creating the timer item",
|
276
|
+
FFI.uv_timer_init(ptr, timer)
|
277
|
+
|
278
|
+
Util.error_check "starting the timer item",
|
279
|
+
FFI.uv_timer_start(timer, callback, timeout, repeat_interval)
|
280
|
+
|
281
|
+
id
|
282
|
+
end
|
283
|
+
|
284
|
+
# Stop handling the given timer.
|
285
|
+
def timer_cancel(id)
|
286
|
+
id = Integer(id)
|
287
|
+
|
288
|
+
timer, callback = @running_timers.delete(id)
|
289
|
+
|
290
|
+
return unless timer
|
291
|
+
|
292
|
+
Util.error_check "stopping the timer item",
|
293
|
+
FFI.uv_timer_stop(timer)
|
294
|
+
|
295
|
+
@available_timers << timer
|
296
|
+
|
297
|
+
nil
|
298
|
+
end
|
299
|
+
|
300
|
+
# Start a timer to run the given block after the given timeout.
|
301
|
+
# The timer will be run just once, starting now.
|
302
|
+
def timer_oneshot(time, &block)
|
303
|
+
timer_start(time, &block)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Start a timer to wake the given fiber after the given timeout.
|
307
|
+
# The timer will be run just once, starting now.
|
308
|
+
def timer_oneshot_wake(time, fiber)
|
309
|
+
timer_start(time) { fiber.resume } # TODO: optimize this case
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
# Callback method called directly from FFI when an event is readable.
|
315
|
+
def _poll_read_callback(poll, rc, events)
|
316
|
+
rescue_abort do
|
317
|
+
fd = @fds_by_read_addr.fetch(poll.address)
|
318
|
+
readables = @on_readables.fetch(fd)
|
319
|
+
|
320
|
+
handler, persistent = readables.last
|
321
|
+
pop_readable(fd) unless persistent
|
322
|
+
|
323
|
+
invoke_handler(handler, rc)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Callback method called directly from FFI when an event is writable.
|
328
|
+
def _poll_write_callback(poll, rc, events)
|
329
|
+
rescue_abort do
|
330
|
+
fd = @fds_by_write_addr.fetch(poll.address)
|
331
|
+
writables = @on_writables.fetch(fd)
|
332
|
+
|
333
|
+
handler, persistent = writables.last
|
334
|
+
pop_writable(fd) unless persistent
|
335
|
+
|
336
|
+
invoke_handler(handler, rc)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Invoke the given handler, possibly converting the given rc to an error.
|
341
|
+
def invoke_handler(handler, rc)
|
342
|
+
case handler
|
343
|
+
when ::Fiber
|
344
|
+
if rc == 0
|
345
|
+
handler.resume nil
|
346
|
+
else
|
347
|
+
handler.resume Util.error_create("running the libuv loop", rc)
|
348
|
+
end
|
349
|
+
when ::Proc
|
350
|
+
if rc == 0
|
351
|
+
handler.call nil
|
352
|
+
else
|
353
|
+
error = Util.error_create("running the libuv loop", rc)
|
354
|
+
handler.call error
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module Bran
|
3
|
+
module LibUV
|
4
|
+
module Util
|
5
|
+
|
6
|
+
def self.error_check(action_description, rc)
|
7
|
+
raise error_create(action_description, rc) if rc < 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.error_create(action_description, rc)
|
11
|
+
# TODO: use appropriate SystemCallError exception class based on errno.
|
12
|
+
name = FFI.uv_err_name(rc)
|
13
|
+
desc = FFI.uv_strerror(rc)
|
14
|
+
RuntimeError.new("LibUV error - while #{action_description} - #{name} - #{desc}")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bran
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joe McIlvain
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ffi
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.9.8
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.9'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.9.8
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.6'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.6'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '10.3'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '10.3'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pry
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.9'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.9'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec-its
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: fivemat
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.3'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.3'
|
117
|
+
description: A source of Fiber.
|
118
|
+
email: joe.eli.mac@gmail.com
|
119
|
+
executables: []
|
120
|
+
extensions:
|
121
|
+
- ext/libuv/Rakefile
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- README.md
|
125
|
+
- ext/libuv/Rakefile
|
126
|
+
- lib/bran.rb
|
127
|
+
- lib/bran/ext.rb
|
128
|
+
- lib/bran/ext/io.rb
|
129
|
+
- lib/bran/ext/rainbows.rb
|
130
|
+
- lib/bran/ext/rainbows/bran.rb
|
131
|
+
- lib/bran/ext/tcp_server.rb
|
132
|
+
- lib/bran/fiber_manager.rb
|
133
|
+
- lib/bran/libuv.rb
|
134
|
+
- lib/bran/libuv/ffi.rb
|
135
|
+
- lib/bran/libuv/reactor.rb
|
136
|
+
- lib/bran/libuv/util.rb
|
137
|
+
homepage: https://github.com/jemc/ruby-bran
|
138
|
+
licenses:
|
139
|
+
- All rights reserved.
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.4.5
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: bran
|
161
|
+
test_files: []
|
162
|
+
has_rdoc:
|