pants 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ require 'eventmachine'
2
+ require_relative 'base_reader'
3
+
4
+
5
+ class Pants
6
+ module Readers
7
+ # This is the EventMachine connection that reads the source file and puts
8
+ # the read data into the data channel so writers can write as they need to.
9
+ class FileReaderConnection < EventMachine::Connection
10
+ include LogSwitch::Mixin
11
+
12
+ # @param [EventMachine::Channel] write_to_channel The data channel to write
13
+ # read data to.
14
+
15
+ # @param [EventMachine::Callback] starter Gets called when the
16
+ # it's been fulling initialized.
17
+ #
18
+ # @param [EventMachine::Callback] stopper Gets called when the
19
+ # file-to-read has been fully read.
20
+ def initialize(write_to_channel, starter, stopper)
21
+ @write_to_channel = write_to_channel
22
+ @stopper = stopper
23
+ @starter = starter
24
+ end
25
+
26
+ def post_init
27
+ @starter.call
28
+ end
29
+
30
+ # Reads the data and writes it to the data channel.
31
+ #
32
+ # @param [String] data The file data to write to the channel.
33
+ def receive_data(data)
34
+ log "<< #{data.size}"
35
+ @write_to_channel << data
36
+ end
37
+
38
+ # Called when the file is done being read.
39
+ def unbind
40
+ log "Unbinding, done writing, and notifying the stopper..."
41
+ @stopper.call
42
+ end
43
+ end
44
+
45
+
46
+ # This is the interface for FileReaderConnections. It controls starting and
47
+ # stopping the connection.
48
+ class FileReader < BaseReader
49
+ include LogSwitch::Mixin
50
+
51
+ # @return [String] Path to the file that's being read.
52
+ attr_reader :file_path
53
+
54
+ # @param [String] file_path Path to the file to read.
55
+ #
56
+ # @param [EventMachine::Callback] core_stopper_callback The Callback that will get
57
+ # called when #stopper is called. #stopper is called when the whole
58
+ # file has been read and pushed to the channel.
59
+ def initialize(file_path, core_stopper_callback)
60
+ log "Initializing #{self.class} with file path '#{file_path}'"
61
+ @read_object = file_path
62
+ @file_path = file_path
63
+
64
+ log "Opening file '#{@file_path}'"
65
+ @file = File.open(@file_path, 'r')
66
+
67
+ super(core_stopper_callback)
68
+ end
69
+
70
+ # Starts reading the file after all writers have been started.
71
+ def start
72
+ callback = EM.Callback do
73
+ log "Adding file '#{@file_path}'..."
74
+ EM.attach(@file, FileReaderConnection, @write_to_channel, starter, stopper)
75
+ end
76
+
77
+ super(callback)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,80 @@
1
+ require 'eventmachine'
2
+ require_relative 'base_reader'
3
+ require_relative '../network_helpers'
4
+
5
+
6
+ class Pants
7
+ module Readers
8
+
9
+ # This is the EventMachine connection that reads on the source IP and UDP
10
+ # port. It places all read data onto the data channel. Allows for unicast or
11
+ # multicast addresses; it'll detect which to use from the IP you pass in.
12
+ class UDPReaderConnection < EventMachine::Connection
13
+ include LogSwitch::Mixin
14
+ include Pants::NetworkHelpers
15
+
16
+ # @param [EventMachine::Channel] write_to_channel The data channel to write
17
+ # read data to.
18
+ #
19
+ # @param [EventMachine::Callback] starter_callback The callback that
20
+ # should get called when the connection has been fully initialized.
21
+ def initialize(write_to_channel, starter_callback)
22
+ @write_to_channel = write_to_channel
23
+ @starter_callback = starter_callback
24
+ port, ip = Socket.unpack_sockaddr_in(get_sockname)
25
+
26
+ if Addrinfo.ip(ip).ipv4_multicast? || Addrinfo.ip(ip).ipv6_multicast?
27
+ log "Got a multicast address: #{ip}:#{port}"
28
+ setup_multicast_socket(ip)
29
+ else
30
+ log "Got a unicast address: #{ip}:#{port}"
31
+ end
32
+ end
33
+
34
+ def post_init
35
+ @starter_callback.call
36
+ end
37
+
38
+ # Reads the data and writes it to the data channel.
39
+ #
40
+ # @param [String] data The socket data to write to the channel.
41
+ def receive_data(data)
42
+ @write_to_channel << data
43
+ end
44
+ end
45
+
46
+
47
+ # This is the interface for UDPReaderConnections. It controls what happens
48
+ # when the you want to start it up and stop it.
49
+ class UDPReader < BaseReader
50
+ include LogSwitch::Mixin
51
+
52
+ # @param [String] host The IP address to read on.
53
+ #
54
+ # @param [Fixnum] port The UDP port to read on.
55
+ #
56
+ # @param [EventMachine::Callback] core_stopper_callback The Callback that will get
57
+ # called when #stopper is called. Since there is no clear end to when
58
+ # to stop reading this I/O, #stopper is never called internally; it must
59
+ # be called externally.
60
+ def initialize(host, port, core_stopper_callback)
61
+ @read_object = "udp://#{host}:#{port}"
62
+ @host = host
63
+ @port = port
64
+
65
+ super(core_stopper_callback)
66
+ end
67
+
68
+ # Starts reading on the UDP IP and port and pushing packets to the channel.
69
+ def start
70
+ callback = EM.Callback do
71
+ log "Adding a #{self.class} at #{@host}:#{@port}..."
72
+ EM.open_datagram_socket(@host, @port, UDPReaderConnection,
73
+ @write_to_channel, starter)
74
+ end
75
+
76
+ super(callback)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,120 @@
1
+ require_relative 'readers/base_reader'
2
+
3
+
4
+ class Pants
5
+
6
+ # A Seam is a core Pants object type (like Readers and Writers) that lets you
7
+ # attach to a Reader, work with the read data, and pass it on to attached
8
+ # Writers. It implements buffering by using EventMachine Queues: pop data
9
+ # off the @read_queue, work with it, then push it onto the @write_queue. Once
10
+ # on the @write_queue, the Seam will pass on to all Writers that have been
11
+ # added to it.
12
+ #
13
+ # The @read_queue is wrapped by #read_items, which yields data
14
+ # chunks from the Reader in, allowing easy access to each bit of data as it
15
+ # was when it was read in. The @write_queue is wrapped by #write, which
16
+ # lets you just give it the data you want to pass on to the attached Writers.
17
+ #
18
+ # Seams are particularly useful for working with network data, where if you're
19
+ # redirecting traffic from one place to another, you may need to alter data
20
+ # in those packets to make it useful to the receiving ends.
21
+ class Seam < Pants::Readers::BaseReader
22
+ include LogSwitch::Mixin
23
+
24
+ # @param [EventMachine::Callback] core_stopper_callback The callback that's
25
+ # provided by Core.
26
+ #
27
+ # @param [EventMachine::Channel] reader_channel The channel from the Reader
28
+ # that the Seam is attached to.
29
+ def initialize(core_stopper_callback, reader_channel)
30
+ @read_queue = EM::Queue.new
31
+ @write_queue = EM::Queue.new
32
+ @write_object ||= nil
33
+
34
+ @receives = 0
35
+ @reads = 0
36
+ @writes = 0
37
+ @sends = 0
38
+
39
+ reader_channel.subscribe do |data|
40
+ log "Got data on reader channel"
41
+ @read_queue << data
42
+ @receives += data.size
43
+ end
44
+
45
+ super(core_stopper_callback)
46
+ send_data
47
+ end
48
+
49
+ def start(callback)
50
+ super(callback)
51
+
52
+ starter.call
53
+ end
54
+
55
+ # Make sure you call this (with super()) in your child to ensure read and
56
+ # write queues are flushed.
57
+ def stop
58
+ log "Stopping..."
59
+ log "receives #{@receives}"
60
+ log "reads #{@reads}"
61
+ log "writes #{@writes}"
62
+ log "sends #{@sends}"
63
+
64
+ finish_loop = EM.tick_loop do
65
+ if @read_queue.empty? && @write_queue.empty?
66
+ :stop
67
+ end
68
+ end
69
+
70
+ finish_loop.on_stop { stopper.call }
71
+ end
72
+
73
+ # @return [String] A String that identifies what the writer is writing to.
74
+ # This is simply used for displaying info to the user.
75
+ def write_object
76
+ if @write_object
77
+ @write_object
78
+ else
79
+ warn "No write_object info has been defined for this writer."
80
+ end
81
+ end
82
+
83
+ # Call this to read data that was put into the read queue. It yields one
84
+ # "item" (however the data was put onto the queue) at a time. It will
85
+ # continually yield as there is data that comes in on the queue.
86
+ #
87
+ # @param [Proc] block The block to yield items from the reader to.
88
+ # @yield [item] Gives one item off the read queue.
89
+ def read_items(&block)
90
+ processor = proc do |item|
91
+ block.call(item)
92
+ @reads += item.size
93
+ @read_queue.pop(&processor)
94
+ end
95
+
96
+ @read_queue.pop(&processor)
97
+ end
98
+
99
+ # Call this after your Seam child has processed data and is ready to send it
100
+ # to its writers.
101
+ #
102
+ # @param [Object] data
103
+ def write(data)
104
+ @write_queue << data
105
+ @writes += data.size
106
+ end
107
+
108
+ private
109
+
110
+ def send_data
111
+ processor = proc do |data|
112
+ @write_to_channel << data
113
+ @sends += data.size
114
+ @write_queue.pop(&processor)
115
+ end
116
+
117
+ @write_queue.pop(&processor)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,3 @@
1
+ class Pants
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,73 @@
1
+ require 'eventmachine'
2
+ require_relative '../logger'
3
+
4
+
5
+ class Pants
6
+ module Writers
7
+
8
+ # Provides conventions for creating your own writer that can stop and start
9
+ # safely.
10
+ #
11
+ # You should also consider adding attr_readers/methods for attributes that
12
+ # are differentiators from other writers of the same type. This will allow
13
+ # readers to more easily remove your writer from them.
14
+ class BaseWriter
15
+
16
+ # @param [EventMachine::Channel] read_from_channel The channel that this
17
+ # writer should read from.
18
+ def initialize(read_from_channel)
19
+ @running = false
20
+ @read_from_channel = read_from_channel
21
+ @write_object ||= nil
22
+ @starter = nil
23
+ @stopper = nil
24
+ end
25
+
26
+ # This method must be redefined in a child class. The reader that this
27
+ # writer is tied to will call this before it starts reading.
28
+ def start
29
+ warn "You haven't defined a start method--are you sure this writer does something?"
30
+ end
31
+
32
+ # This method must be redefined in a child class. The reader that this
33
+ # writer is tied to will call this when it's done reading whatever it's
34
+ # reading.
35
+ def stop
36
+ warn "You haven't defined a stop method--are you sure you're cleaning up?"
37
+ end
38
+
39
+ # @return [String] A String that identifies what the writer is writing to.
40
+ # This is simply used for displaying info to the user.
41
+ def write_object
42
+ if @write_object
43
+ @write_object
44
+ else
45
+ warn "No write_object info has been defined for this writer."
46
+ end
47
+ end
48
+
49
+ # This should get called with #call after the writer is sure to be up
50
+ # and running, ready for accepting data.
51
+ #
52
+ # @return [EventMachine::Callback] The Callback that should get
53
+ # called.
54
+ def starter
55
+ @starter ||= EM.Callback { @running = true }
56
+ end
57
+
58
+ # This should get called with #call after the writer is done writing
59
+ # out the data in its channel.
60
+ #
61
+ # @return [EventMachine::Callback] The Callback that should get
62
+ # called.
63
+ def stopper
64
+ @stopper ||= EM.Callback { @running = false }
65
+ end
66
+
67
+ # @return [Boolean] Is the Writer writing data?
68
+ def running?
69
+ @running
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'base_writer'
2
+
3
+
4
+ class Pants
5
+ module Writers
6
+
7
+ # This is the interface for FileWriterConnections. It controls starting,
8
+ # stopping, and threading the connection.
9
+ class FileWriter < BaseWriter
10
+ include LogSwitch::Mixin
11
+
12
+ # @return [String] The path to the file that's being written to.
13
+ attr_reader :file_path
14
+
15
+ # @param [EventMachine::Channel] read_from_channel The channel to read data
16
+ # from and thus write to file.
17
+ #
18
+ # @param [String] file_path The path to write to.
19
+ def initialize(file_path, read_from_channel)
20
+ @file = file_path.is_a?(File) ? file_path : File.open(file_path, 'w')
21
+ @file_path = file_path
22
+ @write_object = @file_path
23
+
24
+ super(read_from_channel)
25
+ end
26
+
27
+ def stop
28
+ log "Finishing ID #{__id__} and closing file #{@file}"
29
+ @file.close unless @file.closed?
30
+ stopper.call
31
+ end
32
+
33
+ def start
34
+ log "#{__id__} Adding a #{self.class} to write to #{@file_path}"
35
+
36
+ EM.defer do
37
+ @read_from_channel.subscribe do |data|
38
+ begin
39
+ bytes_written = @file.write_nonblock(data)
40
+ log "Wrote normal, #{bytes_written} bytes"
41
+ rescue IOError
42
+ log "Finishing writing; only wrote #{bytes_written}"
43
+
44
+ unless bytes_written == data.size
45
+ File.open(@file, 'a') do |file|
46
+ file.write_nonblock(data)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ start_loop = EM.tick_loop { :stop unless @file.closed? }
53
+ start_loop.on_stop { starter.call }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,125 @@
1
+ require 'socket'
2
+ require_relative 'base_writer'
3
+ require_relative '../network_helpers'
4
+
5
+
6
+ class Pants
7
+ module Writers
8
+
9
+ # This is the EventMachine connection that connects the data from the data
10
+ # channel (put there by the reader you're using) to the IP and UDP port you
11
+ # want to send it to.
12
+ class UDPWriterConnection < EM::Connection
13
+ include LogSwitch::Mixin
14
+ include Pants::NetworkHelpers
15
+
16
+ # Packets get split up before writing if they're over this size.
17
+ PACKET_SPLIT_THRESHOLD = 1400
18
+
19
+ # Packets get split up to this size before writing.
20
+ PACKET_SPLIT_SIZE = 1300
21
+
22
+ # @param [EventMachine::Channel] read_from_channel The channel to expect
23
+ # data on and write to the socket.
24
+ #
25
+ # @param [String] dest_ip The IP address to send data to. Can be unicast
26
+ # or multicast.
27
+ #
28
+ # @param [Fixnum] dest_port The UDP port to send data to.
29
+ def initialize(read_from_channel, dest_ip, dest_port)
30
+ @read_from_channel = read_from_channel
31
+ @dest_ip = dest_ip
32
+ @dest_port = dest_port
33
+
34
+ if Addrinfo.ip(@dest_ip).ipv4_multicast? || Addrinfo.ip(@dest_ip).ipv6_multicast?
35
+ log "Got a multicast address: #{@dest_ip}:#{@dest_port}"
36
+ setup_multicast_socket(@dest_ip)
37
+ else
38
+ log "Got a unicast address: #{@dest_ip}:#{@dest_port}"
39
+ end
40
+ end
41
+
42
+ # Sends data received on the data channel to the destination IP and port.
43
+ # Since data may have been put in to the channel by a File reader (and will
44
+ # therefore be larger chunks of data than you'll want to send in a packet
45
+ # over the wire), it will split packets into +PACKET_SPLIT_SIZE+ sized
46
+ # packets before sending.
47
+ def post_init
48
+ @read_from_channel.subscribe do |data|
49
+ if data.size > PACKET_SPLIT_THRESHOLD
50
+ log "#{__id__} Got big data: #{data.size}. Splitting..."
51
+ io = StringIO.new(data)
52
+ io.binmode
53
+
54
+ begin
55
+ log "#{__id__} Spliced #{PACKET_SPLIT_SIZE} bytes to socket packet"
56
+
57
+ while true
58
+ new_packet = io.read_nonblock(PACKET_SPLIT_SIZE)
59
+ send_datagram(new_packet, @dest_ip, @dest_port)
60
+ new_packet = nil
61
+ end
62
+ rescue EOFError
63
+ send_datagram(new_packet, @dest_ip, @dest_port) if new_packet
64
+ io.close
65
+ end
66
+ else
67
+ log "Sending data to #{@dest_ip}:#{@dest_port}"
68
+ send_datagram(data, @dest_ip, @dest_port)
69
+ end
70
+ end
71
+ end
72
+
73
+ def receive_data(data)
74
+ log "Got data (should I?): #{data.size}, port #{@dest_port}, peer: #{get_peername}"
75
+ end
76
+ end
77
+
78
+
79
+ # This is the interface to UDPWriterConnections. It defines what happens
80
+ # when you want to start it up and stop it.
81
+ class UDPWriter < BaseWriter
82
+ include LogSwitch::Mixin
83
+
84
+ # @return [String] The IP address that's being written to.
85
+ attr_reader :host
86
+
87
+ # @return [Fixnum] The port that's being written to.
88
+ attr_reader :port
89
+
90
+ # @param [String] host
91
+ #
92
+ # @param [Fixnum] port
93
+ #
94
+ # @param [EventMachine::Channel] read_from_channel
95
+ def initialize(host, port, read_from_channel)
96
+ @host = host
97
+ @port = port
98
+ @connection = nil
99
+ @write_object = "udp://#{@host}:#{@port}"
100
+
101
+ super(read_from_channel)
102
+ end
103
+
104
+ # Readies the writer for data to write and waits for data to write.
105
+ def start
106
+ log "#{__id__} Adding a #{self.class} at #{@host}:#{@port}..."
107
+
108
+ EM.defer do
109
+ @connection = EM.open_datagram_socket('0.0.0.0', 0, UDPWriterConnection,
110
+ @read_from_channel, @host, @port)
111
+
112
+ start_loop = EM.tick_loop { :stop if @connection }
113
+ start_loop.on_stop { starter.call }
114
+ end
115
+ end
116
+
117
+ # Closes the connection and notifies the reader that it's done.
118
+ def stop
119
+ log "Finishing ID #{__id__}"
120
+ @connection.close_connection_after_writing
121
+ stopper.call
122
+ end
123
+ end
124
+ end
125
+ end