pants 0.1.0

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,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