distributor 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.
- data/lib/distributor.rb +2 -0
- data/lib/distributor/client.rb +88 -0
- data/lib/distributor/connector.rb +28 -0
- data/lib/distributor/multiplexer.rb +47 -0
- data/lib/distributor/packet.rb +36 -0
- data/lib/distributor/server.rb +66 -0
- data/lib/distributor/version.rb +5 -0
- metadata +62 -0
data/lib/distributor.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require "distributor"
|
2
|
+
require "distributor/connector"
|
3
|
+
require "distributor/multiplexer"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
class Distributor::Client
|
7
|
+
|
8
|
+
def initialize(input, output=input)
|
9
|
+
@connector = Distributor::Connector.new
|
10
|
+
@multiplexer = Distributor::Multiplexer.new(output)
|
11
|
+
@handlers = {}
|
12
|
+
@processes = []
|
13
|
+
@on_close = Hash.new([])
|
14
|
+
|
15
|
+
# reserve a command channel
|
16
|
+
@multiplexer.reserve(0)
|
17
|
+
|
18
|
+
# feed data from the input channel into the multiplexer
|
19
|
+
@connector.handle(input) do |io|
|
20
|
+
@multiplexer.input io
|
21
|
+
end
|
22
|
+
|
23
|
+
@connector.on_close(input) do |io|
|
24
|
+
@multiplexer.output 0, JSON.dump({ "command" => "close", "ch" => ch })
|
25
|
+
end
|
26
|
+
|
27
|
+
# handle the command channel of the multiplexer
|
28
|
+
@connector.handle(@multiplexer.reader(0)) do |io|
|
29
|
+
data = JSON.parse(io.readpartial(4096))
|
30
|
+
|
31
|
+
case command = data["command"]
|
32
|
+
when "close" then
|
33
|
+
ch = data["ch"]
|
34
|
+
@on_close[ch].each { |c| c.call(ch) }
|
35
|
+
when "launch" then
|
36
|
+
ch = data["ch"]
|
37
|
+
@multiplexer.reserve ch
|
38
|
+
@handlers[data["id"]].call(ch)
|
39
|
+
@handlers.delete(data["id"])
|
40
|
+
@processes << ch
|
41
|
+
else
|
42
|
+
raise "no such command: #{command}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def output(ch, data)
|
48
|
+
@multiplexer.output ch, data
|
49
|
+
end
|
50
|
+
|
51
|
+
def run(command, &handler)
|
52
|
+
id = "#{Time.now.to_f}-#{rand(10000)}"
|
53
|
+
@multiplexer.output 0, JSON.dump({ "id" => id, "command" => "run", "args" => command })
|
54
|
+
@handlers[id] = handler
|
55
|
+
end
|
56
|
+
|
57
|
+
def hookup(ch, input, output)
|
58
|
+
# handle data incoming on the multiplexer
|
59
|
+
@connector.handle(@multiplexer.reader(ch)) do |io|
|
60
|
+
begin
|
61
|
+
data = io.readpartial(4096)
|
62
|
+
# output.write "#{ch}: #{data}"
|
63
|
+
output.write data
|
64
|
+
rescue EOFError
|
65
|
+
@multiplexer.output 0, JSON.dump({ "command" => "close", "ch" => ch })
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# handle data incoming from the input channel
|
70
|
+
@connector.handle(input) do |io|
|
71
|
+
begin
|
72
|
+
data = io.readpartial(4096)
|
73
|
+
@multiplexer.output ch, data
|
74
|
+
rescue EOFError
|
75
|
+
@processes.each { |ch| @multiplexer.close(ch) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_close(ch, &blk)
|
81
|
+
@on_close[ch] << blk
|
82
|
+
end
|
83
|
+
|
84
|
+
def start
|
85
|
+
loop { @connector.listen }
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "distributor"
|
2
|
+
|
3
|
+
class Distributor::Connector
|
4
|
+
|
5
|
+
attr_reader :connections
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@connections = {}
|
9
|
+
@on_close = Hash.new([])
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle(from, &handler)
|
13
|
+
@connections[from] = handler
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_close(from, &handler)
|
17
|
+
@on_close[from] << handler
|
18
|
+
end
|
19
|
+
|
20
|
+
def listen
|
21
|
+
rs, ws = IO.select(@connections.keys)
|
22
|
+
rs.each do |from|
|
23
|
+
@on_close.each { |c| c.call } if from.eof?
|
24
|
+
self.connections[from].call(from)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "distributor"
|
2
|
+
require "distributor/packet"
|
3
|
+
|
4
|
+
class Distributor::Multiplexer
|
5
|
+
|
6
|
+
def initialize(output)
|
7
|
+
@output = output
|
8
|
+
@readers = {}
|
9
|
+
@writers = {}
|
10
|
+
|
11
|
+
@output.sync = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def reserve(ch=nil)
|
15
|
+
ch ||= @readers.keys.length
|
16
|
+
raise "channel already taken: #{ch}" if @readers.has_key?(ch)
|
17
|
+
@readers[ch], @writers[ch] = IO.pipe
|
18
|
+
ch
|
19
|
+
end
|
20
|
+
|
21
|
+
def reader(ch)
|
22
|
+
@readers[ch] || raise("no such channel: #{ch}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def writer(ch)
|
26
|
+
@writers[ch] || raise("no such channel: #{ch}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def input(io)
|
30
|
+
ch, data = Distributor::Packet.parse(io)
|
31
|
+
return if ch.nil?
|
32
|
+
writer(ch).write data
|
33
|
+
rescue IOError
|
34
|
+
@output.write Distributor::Packet.create(0, JSON.dump({ "command" => "close", "ch" => ch }))
|
35
|
+
end
|
36
|
+
|
37
|
+
def output(ch, data)
|
38
|
+
@output.write Distributor::Packet.create(ch, data)
|
39
|
+
rescue Errno::EPIPE
|
40
|
+
end
|
41
|
+
|
42
|
+
def close(ch)
|
43
|
+
writer(ch).close
|
44
|
+
rescue IOError
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "distributor"
|
2
|
+
|
3
|
+
class Distributor::Packet
|
4
|
+
|
5
|
+
PROTOCOL_VERSION = 1
|
6
|
+
|
7
|
+
def self.create(channel, data)
|
8
|
+
packet = "DIST"
|
9
|
+
packet += pack(PROTOCOL_VERSION)
|
10
|
+
packet += pack(channel)
|
11
|
+
packet += pack(data.length)
|
12
|
+
packet += data
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse(io)
|
16
|
+
header = io.read(4)
|
17
|
+
return if header.nil?
|
18
|
+
raise "invalid header" unless header == "DIST"
|
19
|
+
version = unpack(io.read(4))
|
20
|
+
channel = unpack(io.read(4))
|
21
|
+
length = unpack(io.read(4))
|
22
|
+
data = ""
|
23
|
+
data += io.readpartial([4096,length-data.length].min) while data.length < length
|
24
|
+
|
25
|
+
[ channel, data ]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.pack(num)
|
29
|
+
[num.to_s(16).rjust(8,"0")].pack("H8")
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.unpack(string)
|
33
|
+
string.unpack("N").first
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "distributor"
|
2
|
+
require "distributor/connector"
|
3
|
+
require "distributor/multiplexer"
|
4
|
+
require "pty"
|
5
|
+
|
6
|
+
class Distributor::Server
|
7
|
+
|
8
|
+
def initialize(input, output=input)
|
9
|
+
@connector = Distributor::Connector.new
|
10
|
+
@multiplexer = Distributor::Multiplexer.new(output)
|
11
|
+
|
12
|
+
# reserve a command channel
|
13
|
+
@multiplexer.reserve(0)
|
14
|
+
|
15
|
+
# feed data from the input channel into the multiplexer
|
16
|
+
@connector.handle(input) do |io|
|
17
|
+
@multiplexer.input io
|
18
|
+
end
|
19
|
+
|
20
|
+
# handle the command channel of the multiplexer
|
21
|
+
@connector.handle(@multiplexer.reader(0)) do |io|
|
22
|
+
data = JSON.parse(io.readpartial(4096))
|
23
|
+
|
24
|
+
case command = data["command"]
|
25
|
+
when "close" then
|
26
|
+
@multiplexer.close data["ch"]
|
27
|
+
when "run" then
|
28
|
+
ch = run(data["args"])
|
29
|
+
@multiplexer.output 0, JSON.dump({ "id" => data["id"], "command" => "launch", "ch" => ch })
|
30
|
+
else
|
31
|
+
raise "no such command: #{command}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(command)
|
37
|
+
ch = @multiplexer.reserve
|
38
|
+
|
39
|
+
rd, wr, pid = PTY.spawn(command)
|
40
|
+
|
41
|
+
# handle data incoming from process
|
42
|
+
@connector.handle(rd) do |io|
|
43
|
+
begin
|
44
|
+
@multiplexer.output(ch, io.readpartial(4096))
|
45
|
+
rescue EOFError
|
46
|
+
@multiplexer.output 0, JSON.dump({ "command" => "close", "ch" => ch })
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# handle data incoming on the multiplexer
|
51
|
+
@connector.handle(@multiplexer.reader(ch)) do |input_io|
|
52
|
+
data = input_io.readpartial(4096)
|
53
|
+
wr.write data
|
54
|
+
end
|
55
|
+
|
56
|
+
ch
|
57
|
+
end
|
58
|
+
|
59
|
+
def close(ch)
|
60
|
+
end
|
61
|
+
|
62
|
+
def start
|
63
|
+
loop { @connector.listen }
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: distributor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Dollar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: &70336779621500 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.13.6
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70336779621500
|
25
|
+
description: TCP Multiplexer
|
26
|
+
email: ddollar@gmail.com
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files: []
|
30
|
+
files:
|
31
|
+
- lib/distributor/client.rb
|
32
|
+
- lib/distributor/connector.rb
|
33
|
+
- lib/distributor/multiplexer.rb
|
34
|
+
- lib/distributor/packet.rb
|
35
|
+
- lib/distributor/server.rb
|
36
|
+
- lib/distributor/version.rb
|
37
|
+
- lib/distributor.rb
|
38
|
+
homepage: http://github.com/ddollar/distributor
|
39
|
+
licenses: []
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.8.11
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: TCP Multiplexer
|
62
|
+
test_files: []
|