gmalamid-spinal 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 nutrun.com, Andy Kent, George Malamidis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,12 @@
1
+ --- It's still very early days and this is experimental code, read: "Here be dragons" ---
2
+
3
+ Spinal is lightweight middleware for bidirectional process distribution over TCP.
4
+ It comes in three parts all currently implemented in Ruby:
5
+
6
+ spinald - This is the server daemon, it manages connections and distributes requests. Servers are cluster-able in a share nothing style architecture.
7
+
8
+ Spinal Client Library - The client Library makes it super simple to delegate work to a Spinal Service, requests can be blocking or asynchronous.
9
+
10
+ Spinal Service Library - Use the service library to implement workers. As with servers, services are cluster-able and distributed.
11
+
12
+ See etc/demo for a simplified example using Rack as a client to a directory service.
@@ -0,0 +1,16 @@
1
+ require "rubygems"
2
+ require "rake/testtask"
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.pattern = "test/*_test.rb"
8
+ end
9
+
10
+ task(:check_gemspec) do
11
+ require 'rubygems/specification'
12
+ data = File.read('spinal.gemspec')
13
+ spec = nil
14
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
15
+ puts spec
16
+ end
@@ -0,0 +1,8 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'spinal', 'spinald')
2
+
3
+ Spinal.debug = true
4
+
5
+ EM.run do
6
+ trap(:INT) {EM.stop}
7
+ EM.start_server("localhost", 8888, Spinal::Spinald)
8
+ end
@@ -0,0 +1,22 @@
1
+ require "rubygems"
2
+ require "thin"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..', '..', 'lib')
5
+
6
+ require "spinal/thin_deferrable_connection"
7
+ require "spinal/rack_remote_query"
8
+
9
+ Thin::Connection.send(:include, Spinal::ThinDeferrableConnection)
10
+
11
+ class Webapp
12
+ include Spinal::Rack::RemoteQuery
13
+
14
+ def call(env)
15
+ request = Rack::Request.new(env)
16
+ remote_query('wiretap', request.params.to_yaml) do |response|
17
+ [response.status, {'Content-Type' => 'text/plain'}, response.body]
18
+ end
19
+ end
20
+ end
21
+
22
+ Rack::Handler::Thin.run(Webapp.new, :Host => 'localhost', :Port => 4567)
@@ -0,0 +1,26 @@
1
+ Requests
2
+
3
+ Query a resource identified by URI
4
+ ========================
5
+ QUERY
6
+ address
7
+ ASCII payload
8
+ ___END___
9
+ ========================
10
+
11
+ Register Service with Spinal on resource identified by URI
12
+ ========================
13
+ REGISTER
14
+ address
15
+ host:port
16
+ ___END___
17
+ ========================
18
+
19
+
20
+ Responses
21
+ ========================
22
+ RESPONSE
23
+ [200|400|...]
24
+ ASCII body
25
+ ___END___
26
+ ========================
@@ -0,0 +1,28 @@
1
+ require "socket"
2
+ require "fcntl"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..')
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ module Client
10
+ class Blocking
11
+ def initialize(host, port)
12
+ @socket = TCPSocket.new(host, port.to_i)
13
+ @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
14
+ end
15
+
16
+ def query(address, payload)
17
+ packet = Packet::Query.new(address, payload)
18
+ @socket.write(packet.raw)
19
+ packet_parser, resp = PacketParser.new, []
20
+ loop do
21
+ str = @socket.gets("\r\n").strip
22
+ packet_parser.parse(str)
23
+ return packet_parser.packet if packet_parser.has_packet?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..')
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ module Client
10
+ module Evented
11
+ def query(uri, payload = '', &response)
12
+ send_data(Packet::Query.new(uri, payload).raw)
13
+ @response = response
14
+ end
15
+
16
+ def receive_data(data)
17
+ packet_parser.parse(data)
18
+ packet_parser.each_packet { |packet| @response.call(packet) }
19
+ end
20
+
21
+ def packet_parser
22
+ @packet_parser ||= PacketParser.new
23
+ end
24
+
25
+ def self.connect(opts={})
26
+ client = nil
27
+ EM::safe_run(opts[:background]) do
28
+ client = EM.connect(opts[:host] || '0.0.0.0', opts[:port] || 8888, self)
29
+ end
30
+ client
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module EventMachine
37
+ def EventMachine::safe_run(background = nil, &block)
38
+ if EM::reactor_running?
39
+ EM::next_tick(&block)
40
+ sleep unless background
41
+ else
42
+ if background
43
+ $em_reactor_thread = Thread.new do
44
+ EM::run(&block)
45
+ end
46
+ else
47
+ EM::run(&block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ module Spinal
2
+ module Rack
3
+ module RemoteQuery
4
+ def remote_query(address, payload='', &response_callback)
5
+ @response = Spinal::ThinDeferrableConnection::DeferredResponse.new(&response_callback)
6
+ connection.send_data(Packet::Query.new(address, payload).raw)
7
+ @response
8
+ end
9
+
10
+ def receive_packet(packet)
11
+ @response.deliver(packet)
12
+ end
13
+
14
+ def connection
15
+ @connection ||= EM.connect('0.0.0.0', 8888, SpinalConnection, self)
16
+ end
17
+
18
+ module SpinalConnection
19
+ def initialize(client)
20
+ super
21
+ @client = client
22
+ end
23
+
24
+ def receive_data(data)
25
+ packet_parser.parse(data)
26
+ packet_parser.each_packet { |packet| @client.receive_packet(packet) }
27
+ end
28
+
29
+ def packet_parser
30
+ @packet_parser ||= PacketParser.new
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.dirname(__FILE__)
5
+
6
+ require "shared"
7
+ require "service/registration"
8
+ require "service/connection"
9
+ require "service/runner"
10
+ require "service/request"
11
+
12
+ module Spinal
13
+ module Service
14
+ def call(request)
15
+ raise "A Spinal::Service must implement the call(request) method"
16
+ end
17
+
18
+ module ClassMethods
19
+ def run_as(address, opts={})
20
+ opts[:address] = address
21
+ Runner.new(self.new, opts).run
22
+ end
23
+ end
24
+
25
+ def self.included(target)
26
+ target.extend(ClassMethods)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Spinal
2
+ module Service
3
+ module Connection
4
+ attr_writer :service, :servers, :address, :host, :port
5
+
6
+ def receive_data(data)
7
+ packet_parser.parse(data)
8
+ packet_parser.each_packet do |packet|
9
+ request = Request.new(@service)
10
+ request.callback {|response| send_data(response)}
11
+ request.handle(packet)
12
+ end
13
+ end
14
+
15
+ def service
16
+ @service || proc {|req| "No service specified."}
17
+ end
18
+
19
+ def packet_parser
20
+ @packet_parser ||= PacketParser.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ module Spinal
2
+ module Registration
3
+ def receive_data(data)
4
+ packet_parser.parse(data)
5
+ packet_parser.each_packet do |packet|
6
+ @status = packet.status
7
+ end
8
+ end
9
+
10
+ def bind(servers, service_address, service_host, service_port)
11
+ servers.each do |server|
12
+ server_host, server_port = server.split(':')
13
+ conn = EM.connect(server_host, server_port.to_i, self)
14
+ packet = Packet::Register.new(service_address, service_host, service_port).raw
15
+ conn.send_data(packet)
16
+ end
17
+ end
18
+ module_function :bind
19
+
20
+ def unbind
21
+ if @status == StatusCodes::OK
22
+ puts "Registration complete!"
23
+ else
24
+ puts "[WARN] failed to register with spinald"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def packet_parser
31
+ @packet_parser ||= PacketParser.new
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module Spinal
2
+ module Service
3
+ class Request
4
+ include EM::Deferrable
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def handle(packet)
12
+ @packet = packet
13
+ EM.spawn do |request|
14
+ response = begin
15
+ [StatusCodes::OK, request.service.call(request.payload)]
16
+ rescue Spinal::Error => e
17
+ [e.code, e.message]
18
+ rescue Exception => e
19
+ [StatusCodes::SERVER_ERROR, e.message]
20
+ end
21
+ request.set_deferred_success(Packet::Response.new(*response).raw)
22
+ end.notify(self)
23
+ end
24
+
25
+ def payload
26
+ @packet.payload
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ module Spinal
2
+ module Service
3
+ class Runner
4
+ DEFAULT_OPTIONS = {
5
+ :host => "127.0.0.1",
6
+ :port => 5000,
7
+ :servers => ["127.0.0.1:8888"]
8
+ }
9
+
10
+ attr_reader :service
11
+
12
+ def initialize(service, options)
13
+ @service = service
14
+ @options = DEFAULT_OPTIONS.merge(options)
15
+ end
16
+
17
+ def run
18
+ trap(:INT) {EM.stop}
19
+ EM.run do
20
+ EM.start_server(host, port, Spinal::Service::Connection) do |t|
21
+ t.service,t.host, t.port = service, host, port
22
+ end
23
+ puts "#{service} (#{address}) listening on #{host}:#{port}"
24
+ Registration.bind(servers, address, host, port)
25
+ end
26
+ end
27
+
28
+ def host
29
+ @options[:host]
30
+ end
31
+
32
+ def servers
33
+ @options[:servers]
34
+ end
35
+
36
+ def address
37
+ @options[:address]
38
+ end
39
+
40
+ def port
41
+ @options[:port]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require "shared/packet"
4
+ require "shared/packet_parser"
5
+ require "shared/status_codes"
6
+ require "shared/error"
@@ -0,0 +1,12 @@
1
+ module Spinal
2
+ class Error < Exception
3
+ attr_reader :code
4
+ end
5
+
6
+ class NotFound < Error
7
+ def initialize(message)
8
+ super
9
+ @code = Spinal::StatusCodes::NOT_FOUND
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ module Spinal
2
+ module Packet
3
+ BOUNDARY = "___END___" unless defined?(BOUNDARY)
4
+ CR = "\r\n" unless defined?(CR)
5
+
6
+ def self.from_a(ary)
7
+ type = ary.shift
8
+ case type
9
+ when "QUERY" : Query.from_a(ary)
10
+ when "RESPONSE" : Response.from_a(ary)
11
+ when "REGISTER" : Register.from_a(ary)
12
+ else raise("Unrecognized packet type '#{type}'")
13
+ end
14
+ end
15
+
16
+ class Query
17
+ attr_reader :address, :payload
18
+
19
+ def initialize(address, payload)
20
+ @address, @payload = address, payload
21
+ end
22
+
23
+ def self.from_a(ary)
24
+ self.new(ary.shift, ary.join(CR))
25
+ end
26
+
27
+ def raw
28
+ ["QUERY", @address, @payload, "#{BOUNDARY}#{CR}"] * CR
29
+ end
30
+ end
31
+
32
+
33
+ class Response
34
+ attr_reader :status, :body
35
+
36
+ def initialize(status, body = '')
37
+ @status, @body = status.to_i, body
38
+ end
39
+
40
+ def self.from_a(ary)
41
+ self.new(ary.shift, ary.join(CR))
42
+ end
43
+
44
+ def raw
45
+ ["RESPONSE", @status, @body, "#{BOUNDARY}#{CR}"] * CR
46
+ end
47
+ end
48
+
49
+ class Register
50
+ attr_reader :address, :host, :port
51
+
52
+ def initialize(address, host, port)
53
+ @address, @host, @port = address, host, port.to_i
54
+ end
55
+
56
+ def self.from_a(ary)
57
+ address = ary.shift
58
+ host, port = ary.shift.split(":")
59
+ self.new(address, host, port)
60
+ end
61
+
62
+ def raw
63
+ ["REGISTER", @address, "#{@host}:#{@port}", "#{BOUNDARY}#{CR}"] * CR
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ module Spinal
2
+ class PacketParser
3
+ def initialize
4
+ @packets, @data = [], ''
5
+ end
6
+
7
+ def parse(packet)
8
+ @data << packet
9
+ tokens, remainder = [], ''
10
+ #FIXME: OUCH!! This will iterate through the entire payload/body...
11
+ @data.split(Packet::CR).each do |token|
12
+ unless token == Packet::BOUNDARY
13
+ tokens << token
14
+ remainder << "#{token}#{Packet::CR}"
15
+ else
16
+ @packets << Packet.from_a(tokens)
17
+ tokens, remainder = [], ''
18
+ end
19
+ end
20
+ @data = remainder
21
+ end
22
+
23
+ def each_packet
24
+ while packet = @packets.pop
25
+ yield packet
26
+ end
27
+ end
28
+
29
+ def packet
30
+ @packets.pop
31
+ end
32
+
33
+ def has_packet?
34
+ @packets.size == 1
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ module Spinal
2
+ module StatusCodes
3
+ OK = 200
4
+ NOT_FOUND = 404
5
+ UNKNOWN_RESOURCE = 418
6
+ SERVER_ERROR = 500
7
+ end
8
+ end
@@ -0,0 +1,118 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.dirname(__FILE__)
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ #FIXME: Tmp hacks
10
+ def debug(str) puts str if @debug end
11
+ module_function :debug
12
+ def debug=(bool) @debug = bool end
13
+ module_function :debug=
14
+
15
+ # Keep tmp hacks above this line
16
+ # ==================================================
17
+
18
+ module Spinald
19
+
20
+ def receive_data(data)
21
+ Spinal.debug "Spinal received: #{data}"
22
+ packet_parser.parse(data)
23
+ packet_parser.each_packet { |packet| handle_request(packet) }
24
+ end
25
+
26
+ def handle_request(packet)
27
+ case packet.class.name
28
+ when 'Spinal::Packet::Query' : handle_query(packet)
29
+ when 'Spinal::Packet::Register' : handle_registration(packet)
30
+ else raise("Unknown packet type: #{packet.class.inspect}")
31
+ end
32
+ end
33
+
34
+ def handle_query(packet)
35
+ if resource = resources[packet.address]
36
+ resource.request(packet.raw) { |resp| send_data(resp) }
37
+ else
38
+ send_data(Packet::Response.new(StatusCodes::UNKNOWN_RESOURCE).raw)
39
+ end
40
+ end
41
+
42
+ def handle_registration(packet)
43
+ resources[packet.address] ||= Resource.new(packet.address)
44
+ resources[packet.address].add_service(packet.host, packet.port)
45
+ send_data(Packet::Response.new(StatusCodes::OK).raw)
46
+ close_connection_after_writing
47
+ end
48
+
49
+ def resources
50
+ @@resources ||= {}
51
+ end
52
+
53
+ def packet_parser
54
+ @packet_parser ||= PacketParser.new
55
+ end
56
+
57
+ class Resource
58
+ def initialize(address)
59
+ @address = address
60
+ @queue = []
61
+ @pool = ServiceConnectionPool.new
62
+ end
63
+
64
+ def add_service(host, port)
65
+ @pool.connect_service(host, port)
66
+ end
67
+
68
+ def request(data, &reply_callback)
69
+ if conn = @pool.reserve
70
+ conn.reply &reply_callback
71
+ conn.release do
72
+ @pool.release(conn)
73
+ if queued_request = @queue.pop
74
+ request(queued_request[0], &queued_request[1])
75
+ end
76
+ end
77
+ conn.send_data(data)
78
+ else
79
+ @queue.unshift([data, reply_callback])
80
+ end
81
+ end
82
+ end
83
+
84
+ class ServiceConnectionPool
85
+ def initialize
86
+ @connections = []
87
+ end
88
+
89
+ def connect_service(host, port)
90
+ @connections.unshift(EM.connect(host, port, ServiceConnection))
91
+ end
92
+
93
+ def reserve
94
+ @connections.pop
95
+ end
96
+
97
+ def release(conn)
98
+ @connections.unshift(conn)
99
+ end
100
+ end
101
+
102
+ module ServiceConnection
103
+ def receive_data(data)
104
+ Spinal.debug "ServiceConnection received: #{data}"
105
+ @reply.call(data)
106
+ @release.call
107
+ end
108
+
109
+ def reply(&cb)
110
+ @reply = cb
111
+ end
112
+
113
+ def release(&cb)
114
+ @release = cb
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,40 @@
1
+ $: << File.dirname(__FILE__)
2
+ require "shared"
3
+
4
+ module Spinal
5
+ module ThinDeferrableConnection
6
+ def process
7
+ if threaded?
8
+ @request.threaded = true
9
+ EventMachine.defer(method(:pre_process), method(:post_process))
10
+ else
11
+ @request.threaded = false
12
+ pre_process_result = pre_process
13
+ if pre_process_result.is_a?(DeferredResponse)
14
+ pre_process_result.post_process_callback { |res| post_process(res) }
15
+ else
16
+ post_process(pre_process_result)
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.included(target)
22
+ target.send(:remove_method, :process)
23
+ end
24
+
25
+ class DeferredResponse
26
+ def initialize(&response_callback)
27
+ @response_callback = response_callback
28
+ end
29
+
30
+ def post_process_callback(&post_process_callback)
31
+ @post_process_callback = post_process_callback
32
+ end
33
+
34
+ def deliver(packet)
35
+ result = @response_callback.call(packet)
36
+ @post_process_callback.call(result)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ GEMSPEC =Gem::Specification.new do |s|
2
+ s.name = 'spinal'
3
+ s.version = '0.0.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.rubyforge_project = "spinal"
6
+ s.summary, s.description = 'Lightweight middleware for bidirectional process distribution over TCP'
7
+ s.authors = 'Andy Kent, George Malamidis'
8
+ s.email = 'george@nutrun.com'
9
+ # s.homepage = 'http://spinal.rubyforge.org'
10
+ s.has_rdoc = false
11
+ s.rdoc_options += ['--quiet', '--title', 'Spinal', '--main', 'README']
12
+ s.extra_rdoc_files = ['README', 'COPYING']
13
+ s.files = [
14
+ "COPYING",
15
+ "Rakefile",
16
+ "README",
17
+ "spinal.gemspec",
18
+ "etc/demo/daemon.rb",
19
+ "etc/demo/service.rb",
20
+ "etc/demo/web.rb",
21
+ "etc/protocol.txt",
22
+ "lib/spinal/client/blocking.rb",
23
+ "lib/spinal/client/evented.rb",
24
+ "lib/spinal/rack_remote_query.rb",
25
+ "lib/spinal/service/connection.rb",
26
+ "lib/spinal/service/registration.rb",
27
+ "lib/spinal/service/request.rb",
28
+ "lib/spinal/service/runner.rb",
29
+ "lib/spinal/service.rb",
30
+ "lib/spinal/shared/error.rb",
31
+ "lib/spinal/shared/packet.rb",
32
+ "lib/spinal/shared/packet_parser.rb",
33
+ "lib/spinal/shared/status_codes.rb",
34
+ "lib/spinal/shared.rb",
35
+ "lib/spinal/spinald.rb",
36
+ "lib/spinal/thin_deferrable_connection.rb"
37
+ ]
38
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gmalamid-spinal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Kent, George Malamidis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: george@nutrun.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ - COPYING
25
+ files:
26
+ - COPYING
27
+ - Rakefile
28
+ - README
29
+ - spinal.gemspec
30
+ - etc/demo/daemon.rb
31
+ - etc/demo/service.rb
32
+ - etc/demo/web.rb
33
+ - etc/protocol.txt
34
+ - lib/spinal/client/blocking.rb
35
+ - lib/spinal/client/evented.rb
36
+ - lib/spinal/rack_remote_query.rb
37
+ - lib/spinal/service/connection.rb
38
+ - lib/spinal/service/registration.rb
39
+ - lib/spinal/service/request.rb
40
+ - lib/spinal/service/runner.rb
41
+ - lib/spinal/service.rb
42
+ - lib/spinal/shared/error.rb
43
+ - lib/spinal/shared/packet.rb
44
+ - lib/spinal/shared/packet_parser.rb
45
+ - lib/spinal/shared/status_codes.rb
46
+ - lib/spinal/shared.rb
47
+ - lib/spinal/spinald.rb
48
+ - lib/spinal/thin_deferrable_connection.rb
49
+ has_rdoc: false
50
+ homepage:
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --quiet
54
+ - --title
55
+ - Spinal
56
+ - --main
57
+ - README
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project: spinal
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: Lightweight middleware for bidirectional process distribution over TCP
79
+ test_files: []
80
+