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.
@@ -0,0 +1,2 @@
1
+ module Distributor
2
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Distributor
2
+
3
+ VERSION = "0.0.1"
4
+
5
+ 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: []