pants 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +1 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +42 -0
- data/History.rdoc +6 -0
- data/README.rdoc +339 -0
- data/Rakefile +23 -0
- data/bin/pants +59 -0
- data/lib/pants.rb +84 -0
- data/lib/pants/core.rb +216 -0
- data/lib/pants/error.rb +5 -0
- data/lib/pants/logger.rb +8 -0
- data/lib/pants/network_helpers.rb +25 -0
- data/lib/pants/readers/base_reader.rb +302 -0
- data/lib/pants/readers/file_reader.rb +81 -0
- data/lib/pants/readers/udp_reader.rb +80 -0
- data/lib/pants/seam.rb +120 -0
- data/lib/pants/version.rb +3 -0
- data/lib/pants/writers/base_writer.rb +73 -0
- data/lib/pants/writers/file_writer.rb +59 -0
- data/lib/pants/writers/udp_writer.rb +125 -0
- data/pants.gemspec +31 -0
- data/spec/acceptance/file_reads_spec.rb +24 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/matchers/file_same_size_as.rb +20 -0
- data/spec/support/pants.wav +0 -0
- data/spec/unit/pants/core_spec.rb +105 -0
- data/spec/unit/pants/readers/base_reader_spec.rb +340 -0
- data/spec/unit/pants/readers/file_reader_spec.rb +94 -0
- data/spec/unit/pants/version_spec.rb +7 -0
- data/spec/unit/pants/writers/base_writer_spec.rb +35 -0
- data/spec/unit/pants/writers/file_writer_spec.rb +71 -0
- data/spec/unit/pants/writers/udp_writer_spec.rb +146 -0
- data/spec/unit/pants_spec.rb +6 -0
- data/tasks/pantsmark.thor +66 -0
- data/test_readers.rb +43 -0
- data/test_seams.rb +107 -0
- metadata +215 -0
data/bin/pants
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require_relative '../lib/pants'
|
6
|
+
require_relative '../lib/pants/version'
|
7
|
+
|
8
|
+
|
9
|
+
class PantsRunner < Thor
|
10
|
+
desc "dup [URI]", "read on the URI and send to --destinations"
|
11
|
+
long_desc <<-LONGDESC
|
12
|
+
This will read in any data given by the URI and redirect it to all of the I/O\x5
|
13
|
+
objects given by --destinations. Specify all I/O objects as URIs in the form:\x5
|
14
|
+
|
15
|
+
udp://127.0.0.1:1234, file:///home/bob/BarrellRoll.avi
|
16
|
+
|
17
|
+
...although files can be given without using the URI form.
|
18
|
+
|
19
|
+
Separate multiple destinations by a space:
|
20
|
+
|
21
|
+
$ pants udp://127.0.0.1:1234 --dest=udp://239.0.0.1:1234 udp://10.0.0.1:1234
|
22
|
+
|
23
|
+
LONGDESC
|
24
|
+
option :dest, type: :array, required: true
|
25
|
+
option :verbose, type: :boolean
|
26
|
+
def dup(read_uri)
|
27
|
+
Pants.log = options[:verbose] ? true : false
|
28
|
+
|
29
|
+
add_writers = lambda do |reader|
|
30
|
+
options[:dest].each do |write_uri|
|
31
|
+
reader.write_to(write_uri)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Pants.read(read_uri, &add_writers)
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "version", "print the version of pants"
|
39
|
+
def version
|
40
|
+
puts <<-INFO
|
41
|
+
---------------------------
|
42
|
+
| || O || | # => pants v#{Pants::VERSION}
|
43
|
+
--------------------------- # => https://github.com/turboladen/pants
|
44
|
+
| | | | # => #{RUBY_DESCRIPTION}
|
45
|
+
| | | |
|
46
|
+
| |_| |
|
47
|
+
| / \\ |
|
48
|
+
| | | |
|
49
|
+
| | | |
|
50
|
+
| | | |
|
51
|
+
| | | |
|
52
|
+
|__________| |__________|
|
53
|
+
|
54
|
+
INFO
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
PantsRunner.start
|
data/lib/pants.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative 'pants/core'
|
2
|
+
require_relative 'pants/version'
|
3
|
+
Dir[File.dirname(__FILE__) + "/readers/*.rb"].each { |f| require f }
|
4
|
+
Dir[File.dirname(__FILE__) + "/writers/*.rb"].each { |f| require f }
|
5
|
+
require_relative 'pants/seam'
|
6
|
+
|
7
|
+
|
8
|
+
# This base class provides some helpers for doing quick, non-complex reading
|
9
|
+
# and writing. Check docs on readers and writers for more information.
|
10
|
+
class Pants
|
11
|
+
|
12
|
+
DEFAULT_URI_TO_READER_MAP = [
|
13
|
+
{ uri_scheme: nil, klass: Pants::Readers::FileReader, args: [:path] },
|
14
|
+
{ uri_scheme: 'file', klass: Pants::Readers::FileReader, args: [:path] },
|
15
|
+
{ uri_scheme: 'udp', klass: Pants::Readers::UDPReader, args: [:host, :port] }
|
16
|
+
]
|
17
|
+
|
18
|
+
DEFAULT_URI_TO_WRITER_MAP = [
|
19
|
+
{ uri_scheme: nil, klass: Pants::Writers::FileWriter, args: [:path] },
|
20
|
+
{ uri_scheme: 'file', klass: Pants::Writers::FileWriter, args: [:path] },
|
21
|
+
{ uri_scheme: 'udp', klass: Pants::Writers::UDPWriter, args: [:host, :port] }
|
22
|
+
]
|
23
|
+
|
24
|
+
# The list of mappings of URIs to Reader class types. These mappings allow
|
25
|
+
# Pants to look up the URI scheme and find what type of object should be
|
26
|
+
# created when creating a Reader by giving it a URI. It also defines the
|
27
|
+
# arguments that the Reader class takes; these should Symbols that represent
|
28
|
+
# names of methods that can be called on objects of the URI type.
|
29
|
+
#
|
30
|
+
# You can register new mappings here by pushing new mappings to the list.
|
31
|
+
# Mappings should be in the form:
|
32
|
+
# { uri_scheme: 'my_scheme', klass: MyReaderClass, args: [:arg] }
|
33
|
+
#
|
34
|
+
# Note that if you're wanting to add to this list, and URI doesn't recognize
|
35
|
+
# the URI scheme that you're adding for, you'll need to define that within
|
36
|
+
# URI. An example is given here: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html
|
37
|
+
#
|
38
|
+
# If you want to use your own reader but don't want to go through all of this
|
39
|
+
# hassle, you can add your reader using a different method. See the docs for
|
40
|
+
# Pants::Core for more info.
|
41
|
+
#
|
42
|
+
# @return [Array<Hash>] The list of mappings.
|
43
|
+
def self.readers
|
44
|
+
@readers ||= DEFAULT_URI_TO_READER_MAP
|
45
|
+
end
|
46
|
+
|
47
|
+
# The list of mappings of URIs to Writer class types. See the docs for
|
48
|
+
# .readers for more info.
|
49
|
+
#
|
50
|
+
# @return [Array<Hash>] The list of mappings.
|
51
|
+
# @see Pants.readers
|
52
|
+
def self.writers
|
53
|
+
@writers ||= DEFAULT_URI_TO_WRITER_MAP
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convenience method; doing something like:
|
57
|
+
#
|
58
|
+
# pants = Pants::Core.new
|
59
|
+
# reader = pants.read('udp://0.0.0.0:1234')
|
60
|
+
# reader.add_writer('udp://1.2.3.4:5999')
|
61
|
+
# reader.add_writer('udp_data.raw')
|
62
|
+
# pants.run
|
63
|
+
#
|
64
|
+
# ...becomes:
|
65
|
+
#
|
66
|
+
# Pants.read('udp://0.0.0.1234') do |seam|
|
67
|
+
# seam.add_writer('udp://1.2.3.4:5999')
|
68
|
+
# seam.add_writer('udp_data.raw')
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @param [String] uri Takes a URI ('udp://...' or 'file://...') or the path
|
72
|
+
# to a file.
|
73
|
+
def self.read(uri, &block)
|
74
|
+
pants = Pants::Core.new(&block)
|
75
|
+
|
76
|
+
if uri.kind_of? Pants::Readers::BaseReader
|
77
|
+
pants.add_reader(uri)
|
78
|
+
else
|
79
|
+
pants.read(uri)
|
80
|
+
end
|
81
|
+
|
82
|
+
pants.run
|
83
|
+
end
|
84
|
+
end
|
data/lib/pants/core.rb
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'eventmachine'
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'logger'
|
5
|
+
|
6
|
+
Dir[File.dirname(__FILE__) + "/readers/*.rb"].each { |f| require f }
|
7
|
+
Dir[File.dirname(__FILE__) + "/writers/*.rb"].each { |f| require f }
|
8
|
+
require_relative 'seam'
|
9
|
+
|
10
|
+
|
11
|
+
class Pants
|
12
|
+
|
13
|
+
# A single Core object is necessary for Pants to run. Root-level readers are
|
14
|
+
# attached to the core, then writers are attached to those readers. It's
|
15
|
+
# main job, other than giving a home to readers, is to handle the order of
|
16
|
+
# starting up and shutting of the readers and writers so that no data is lost.
|
17
|
+
class Core
|
18
|
+
include LogSwitch::Mixin
|
19
|
+
|
20
|
+
# @return [Array] The list of readers that are reading data.
|
21
|
+
attr_reader :readers
|
22
|
+
|
23
|
+
def initialize(&block)
|
24
|
+
setup_signals
|
25
|
+
@readers = []
|
26
|
+
|
27
|
+
@convenience_block = block
|
28
|
+
end
|
29
|
+
|
30
|
+
# One method of adding a Reader to the Core. Use this method to make code
|
31
|
+
# reader nicer when reading something that's expressed as a URI.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# core = Pants::Core.new
|
35
|
+
# core.read 'udp://10.2.3.4:9000'
|
36
|
+
#
|
37
|
+
# @param [String,URI] uri The URI to the object to read. Can be a file:///,
|
38
|
+
# udp://, or a string with the path to a file.
|
39
|
+
#
|
40
|
+
# @return [Pants::Reader] The newly created reader.
|
41
|
+
def read(uri)
|
42
|
+
begin
|
43
|
+
uri = uri.is_a?(URI) ? uri : URI(uri)
|
44
|
+
rescue URI::InvalidURIError
|
45
|
+
@readers << new_reader_from_uri(nil, callback)
|
46
|
+
else
|
47
|
+
@readers << new_reader_from_uri(uri, callback)
|
48
|
+
end
|
49
|
+
|
50
|
+
@convenience_block.call(@readers.last) if @convenience_block
|
51
|
+
|
52
|
+
@readers.last
|
53
|
+
end
|
54
|
+
|
55
|
+
# One method of adding a Reader to the Core. Use this method to add an
|
56
|
+
# a) already instantiated Reader object, or b) a Reader from a class of
|
57
|
+
# Reader objects.
|
58
|
+
#
|
59
|
+
# @example Add using class and init variables
|
60
|
+
# core = Pants::Core.new
|
61
|
+
# core.add_reader(Pants::Readers::UDPReader, '10.2.3.4', 9000)
|
62
|
+
#
|
63
|
+
# @example Add using an already instantiated Reader object
|
64
|
+
# core = Pants::Core.new
|
65
|
+
# reader = Pants::Readers::UDPReader.new('10.2.3.4', 9000, core.callback)
|
66
|
+
# core.add_reader(reader)
|
67
|
+
#
|
68
|
+
# Notice how using the last method requires you to pass in the core's
|
69
|
+
# callback method--this is probably one reason for avoiding this method of
|
70
|
+
# adding a reader, yet remains available for flexibility.
|
71
|
+
#
|
72
|
+
# @param [Class,Pants::Reader] obj Either the class of a Reader to create,
|
73
|
+
# or an already created Reader object.
|
74
|
+
# @param [*] args Any arguments that need to be used for creating the
|
75
|
+
# Reader.
|
76
|
+
def add_reader(obj, *args)
|
77
|
+
if obj.is_a? Class
|
78
|
+
@readers << obj.new(*args, callback)
|
79
|
+
elsif obj.kind_of? Pants::Readers::BaseReader
|
80
|
+
@readers << obj
|
81
|
+
else
|
82
|
+
raise Pants::Error, "Don't know how to add a reader of type #{obj}"
|
83
|
+
end
|
84
|
+
|
85
|
+
@convenience_block.call(@readers.last) if @convenience_block
|
86
|
+
|
87
|
+
@readers.last
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates an EventMachine::Callback method that other Readers, Writers, and
|
91
|
+
# others can use for letting the Core know when it can shutdown. Those
|
92
|
+
# Readers, Writers, etc. should handle calling this callback when they're
|
93
|
+
# done doing what they need to do.
|
94
|
+
#
|
95
|
+
# @return [EventMachine::Callback]
|
96
|
+
def callback
|
97
|
+
EM.Callback do
|
98
|
+
if @readers.none?(&:running?)
|
99
|
+
EM.stop_event_loop
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Starts the EventMachine reactor, the reader and the writers.
|
105
|
+
def run
|
106
|
+
raise Pants::Error, "No readers added yet" if @readers.empty?
|
107
|
+
|
108
|
+
starter = proc do
|
109
|
+
puts "Pants v#{Pants::VERSION}"
|
110
|
+
puts ">> Reading from #{@readers.size} readers"
|
111
|
+
|
112
|
+
@readers.each_with_index do |reader, i|
|
113
|
+
puts ">> reader#{i}: Starting read on: #{reader.read_object}"
|
114
|
+
puts ">> reader#{i}: Writing to #{reader.writers.size} writers"
|
115
|
+
|
116
|
+
reader.writers.each_with_index do |writer, j|
|
117
|
+
puts ">> reader#{i}writer#{j}: #{writer.write_object}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
EM::Iterator.new(@readers).each do |reader, iter|
|
122
|
+
reader.start
|
123
|
+
iter.next
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
if EM.reactor_running?
|
128
|
+
log "Joining reactor..."
|
129
|
+
starter.call
|
130
|
+
else
|
131
|
+
log "Starting reactor..."
|
132
|
+
EM.run(&starter)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Tells the reader to signal to its writers that it's time to finish.
|
137
|
+
def stop!
|
138
|
+
puts "Stop called. Closing readers and writers..."
|
139
|
+
|
140
|
+
if @readers.none?(&:running?)
|
141
|
+
puts "No readers are running; nothing to do."
|
142
|
+
else
|
143
|
+
puts "Stopping readers:"
|
144
|
+
|
145
|
+
@readers.each do |reader|
|
146
|
+
puts "\t#{reader}" if reader.running?
|
147
|
+
end
|
148
|
+
|
149
|
+
@readers.each(&:stop!)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Stop, then run.
|
154
|
+
def restart
|
155
|
+
stop!
|
156
|
+
puts "Restarting..."
|
157
|
+
run
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
# Register signals:
|
163
|
+
# * TERM & QUIT calls +stop+ to shutdown gracefully.
|
164
|
+
# * INT calls <tt>stop!</tt> to force shutdown.
|
165
|
+
# * HUP calls <tt>restart</tt> to ... surprise, restart!
|
166
|
+
def setup_signals
|
167
|
+
@trapped_count ||= 0
|
168
|
+
|
169
|
+
stopper = proc do
|
170
|
+
@trapped_count += 1
|
171
|
+
stop!
|
172
|
+
|
173
|
+
# Reset count after 5 seconds
|
174
|
+
EM.add_timer(5) { @trapped_count = 0 }
|
175
|
+
end
|
176
|
+
|
177
|
+
trap('INT') do
|
178
|
+
stopper.call
|
179
|
+
abort "Multiple INT signals trapped; aborting!" if @trapped_count > 1
|
180
|
+
end
|
181
|
+
|
182
|
+
trap('TERM') { stopper.call }
|
183
|
+
|
184
|
+
unless !!RUBY_PLATFORM =~ /mswin|mingw/
|
185
|
+
trap('QUIT') { stop! }
|
186
|
+
trap('HUP') { restart }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @param [URI] uri The URI the Reader is mapped to.
|
191
|
+
#
|
192
|
+
# @return [Pants::Reader] An object of the type that's defined by the URI
|
193
|
+
# scheme.
|
194
|
+
#
|
195
|
+
# @raise [Pants::Error] If no Reader is mapped to +scheme+.
|
196
|
+
def new_reader_from_uri(uri, callback)
|
197
|
+
reader_to_use = if uri.nil?
|
198
|
+
Pants.readers.find { |reader| reader[:uri_scheme].nil? }
|
199
|
+
else
|
200
|
+
Pants.readers.find { |reader| reader[:uri_scheme] == uri.scheme }
|
201
|
+
end
|
202
|
+
|
203
|
+
unless reader_to_use
|
204
|
+
raise ArgumentError, "No reader found with URI scheme: #{uri.scheme}"
|
205
|
+
end
|
206
|
+
|
207
|
+
args = if reader_to_use[:args]
|
208
|
+
reader_to_use[:args].map { |arg| uri.send(arg) }
|
209
|
+
else
|
210
|
+
[]
|
211
|
+
end
|
212
|
+
|
213
|
+
reader_to_use[:klass].new(*args, callback)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/lib/pants/error.rb
ADDED
data/lib/pants/logger.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
|
5
|
+
class Pants
|
6
|
+
|
7
|
+
# Methods that are shared by Readers and Writers that deal with networking.
|
8
|
+
module NetworkHelpers
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Sets Socket options to allow for multicasting.
|
13
|
+
#
|
14
|
+
# @param [String] ip The IP address to add to the multicast group.
|
15
|
+
def setup_multicast_socket(ip)
|
16
|
+
set_membership(::IPAddr.new(ip).hton + ::IPAddr.new('0.0.0.0').hton)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [String] membership The network byte ordered String that represents
|
20
|
+
# the IP(s) that should join the membership group.
|
21
|
+
def set_membership(membership)
|
22
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
require_relative '../logger'
|
2
|
+
|
3
|
+
|
4
|
+
class Pants
|
5
|
+
module Readers
|
6
|
+
class BaseReader
|
7
|
+
include LogSwitch::Mixin
|
8
|
+
|
9
|
+
# @return [Array] The list of Writers attached to the Reader.
|
10
|
+
attr_reader :writers
|
11
|
+
|
12
|
+
# @return [EventMachine::Channel] The channel that Writers should subscribe
|
13
|
+
# to.
|
14
|
+
attr_reader :write_to_channel
|
15
|
+
|
16
|
+
# @return [EventMachine::Callback] The callback from Core that should be
|
17
|
+
# called when the Reader is done reading.
|
18
|
+
attr_reader :core_stopper_callback
|
19
|
+
|
20
|
+
# @param [EventMachine::Callback] core_stopper_callback This gets called when all
|
21
|
+
# reading is done and the writers have written out all their data. It
|
22
|
+
# signals to the caller that the job of the reader is all done. For
|
23
|
+
# first level readers (readers that are not Tees), this lets Pants check
|
24
|
+
# all existing Readers to see if they're done, so it can know when to stop
|
25
|
+
# the reactor.
|
26
|
+
#
|
27
|
+
def initialize(core_stopper_callback)
|
28
|
+
@writers = []
|
29
|
+
@write_to_channel = EM::Channel.new
|
30
|
+
@core_stopper_callback = core_stopper_callback
|
31
|
+
@read_object ||= nil
|
32
|
+
@starter = nil
|
33
|
+
@stopper = nil
|
34
|
+
@running = false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Starts all of the writers, then starts the reader. Child readers must
|
38
|
+
# call this to make sure the writers are all running and ready for data
|
39
|
+
# before the reader starts pushing data onto its Channel.
|
40
|
+
#
|
41
|
+
# @param [EventMachine::Callback] callback Once all writers are up and
|
42
|
+
# running, this gets called, letting the caller know all Writers are up
|
43
|
+
# and running. This should contain all code that the child Reader wants
|
44
|
+
# to execute on start.
|
45
|
+
def start(callback)
|
46
|
+
start_loop = EM.tick_loop do
|
47
|
+
if @writers.empty? || @writers.all?(&:running?)
|
48
|
+
:stop
|
49
|
+
end
|
50
|
+
end
|
51
|
+
start_loop.on_stop { callback.call }
|
52
|
+
|
53
|
+
log "Starting writers for reader #{self.__id__}..."
|
54
|
+
EM::Iterator.new(@writers).each do |writer, iter|
|
55
|
+
writer.start
|
56
|
+
iter.next
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Calls the reader's #stopper, thus forcing the reader to shutdown. For
|
61
|
+
# readers that intend to read a finite amount of data, the Reader should
|
62
|
+
# call the #stopper when it's done; for readers that read a non-stop stream
|
63
|
+
# (i.e. like an open socket), this gets called by OS signals (i.e. if you
|
64
|
+
# ctrl-c).
|
65
|
+
def stop!
|
66
|
+
stopper.call
|
67
|
+
end
|
68
|
+
|
69
|
+
# Allows for adding "about me" info, depending on the reader type. This
|
70
|
+
# info is printed out when Pants starts, so you know get confirmation of
|
71
|
+
# what you're about to do. If you don't define this in your reader, nothing
|
72
|
+
# will be printed out.
|
73
|
+
#
|
74
|
+
# @return [String] A String that identifies what the reader is reading
|
75
|
+
# from.
|
76
|
+
def read_object
|
77
|
+
if @read_object
|
78
|
+
@read_object
|
79
|
+
else
|
80
|
+
warn "No read_object info has been defined for this reader."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Boolean]
|
85
|
+
def running?
|
86
|
+
@running
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [String] uri The URI to the object to read. Can be of URI type
|
90
|
+
# that's defined in Pants.writers.
|
91
|
+
#
|
92
|
+
# @return [Pants::Writers::BaseWriter] The newly created writer.
|
93
|
+
def write_to(uri)
|
94
|
+
begin
|
95
|
+
uri = uri.is_a?(URI) ? uri : URI(uri)
|
96
|
+
rescue URI::InvalidURIError
|
97
|
+
@writers << new_writer_from_uri(nil, @write_to_channel)
|
98
|
+
else
|
99
|
+
@writers << new_writer_from_uri(uri, @write_to_channel)
|
100
|
+
end
|
101
|
+
|
102
|
+
@writers.last
|
103
|
+
end
|
104
|
+
|
105
|
+
# One method of adding a Writer to the Reader. Use this method to add an
|
106
|
+
# a) already instantiated Writer object, or b) a Writers from a class of
|
107
|
+
# Writer objects.
|
108
|
+
#
|
109
|
+
# @example Add using class and init variables
|
110
|
+
# core = Pants::Core.new
|
111
|
+
# core.read 'udp://10.2.3.4:9000'
|
112
|
+
# core.add_writer(Pants::Writers::UDPWriter, '10.5.6.7', 9000)
|
113
|
+
#
|
114
|
+
# @example Add using an already instantiated Writer object
|
115
|
+
# core = Pants::Core.new
|
116
|
+
# reader = core.read 'udp://10.2.3.4:9000'
|
117
|
+
# writer = Pants::Writers::UDPWriter.new('10.5.6.7', 9000, reader.write_to_channel)
|
118
|
+
# core.add_writer(writer)
|
119
|
+
#
|
120
|
+
# Notice how using the last method requires you to pass in the channel
|
121
|
+
# that the reader is pushing data to--this is probably one reason for
|
122
|
+
# avoiding this method of adding a writer, yet remains available for
|
123
|
+
# flexibility.
|
124
|
+
#
|
125
|
+
# @param [Class,Pants::Reader] obj Either the class of a Writer to create,
|
126
|
+
# or an already created Writer object.
|
127
|
+
#
|
128
|
+
# @param [*] args Any arguments that need to be used for creating the
|
129
|
+
# Writer.
|
130
|
+
def add_writer(obj, *args)
|
131
|
+
if obj.is_a? Class
|
132
|
+
@writers << obj.new(*args, @write_to_channel)
|
133
|
+
elsif obj.kind_of? Pants::Writers::BaseWriter
|
134
|
+
@writers << obj
|
135
|
+
else
|
136
|
+
raise Pants::Error, "Don't know how to add a writer of type #{obj}"
|
137
|
+
end
|
138
|
+
|
139
|
+
@writers.last
|
140
|
+
end
|
141
|
+
|
142
|
+
# Removes a writer object from the internal list of writers.
|
143
|
+
#
|
144
|
+
# @example Using URI
|
145
|
+
# reader.writers # => [<Pants::Writers::FileWriter @file_path='./testfile'...>]
|
146
|
+
# reader.remove_writer('./testfile')
|
147
|
+
# reader.writers # => []
|
148
|
+
#
|
149
|
+
# @example Using class and args as key/value pairs
|
150
|
+
# reader.writers # => [<Pants::Writers::FileWriter @file_path='./testfile'...>]
|
151
|
+
# reader.remove_writer(Pants::Writers::FileWriter, file_path: './testfile')
|
152
|
+
# reader.writers # => []
|
153
|
+
#
|
154
|
+
# @param [Class] obj Class of the writer to remove.
|
155
|
+
#
|
156
|
+
# @param [Hash] key_value_pairs Keys are methods to be called on each
|
157
|
+
# writer and will be checked to see if the return value from that method
|
158
|
+
# equals the given value.
|
159
|
+
def remove_writer(obj, key_value_pairs=nil)
|
160
|
+
if obj.is_a? Class
|
161
|
+
@writers.delete_if do |writer|
|
162
|
+
writer.is_a?(obj) &&
|
163
|
+
key_value_pairs.all? { |k, v| writer.send(k) == v }
|
164
|
+
end
|
165
|
+
elsif obj.is_a? String
|
166
|
+
writer = begin
|
167
|
+
uri = obj.is_a?(URI) ? obj : URI(obj)
|
168
|
+
rescue URI::InvalidURIError
|
169
|
+
find_writer_from_uri(nil)
|
170
|
+
else
|
171
|
+
find_writer_from_uri(uri)
|
172
|
+
end
|
173
|
+
|
174
|
+
unless writer
|
175
|
+
raise ArgumentError, "No writer found wth URI scheme: #{uri.scheme}"
|
176
|
+
end
|
177
|
+
|
178
|
+
key_value_pairs = if writer[:args]
|
179
|
+
writer[:args].inject({}) do |result, arg|
|
180
|
+
result[arg] = uri.send(arg)
|
181
|
+
|
182
|
+
result
|
183
|
+
end
|
184
|
+
else
|
185
|
+
{}
|
186
|
+
end
|
187
|
+
|
188
|
+
@writers.delete_if do |w|
|
189
|
+
w.is_a?(writer[:klass]) &&
|
190
|
+
key_value_pairs.all? { |k, v| w.send(k) == v }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Allows for adding a Pants::Seam (or child) object to the reader's list
|
196
|
+
# of internal writers. For more info on Seams, see the docs for
|
197
|
+
# Pants::Seam.
|
198
|
+
#
|
199
|
+
# @param [Pants::Seam] klass The class of the Pants::Seam object to
|
200
|
+
# create.
|
201
|
+
#
|
202
|
+
# @return [Pants::Seam] The seam that was just created.
|
203
|
+
#
|
204
|
+
# @see Pants::Seam
|
205
|
+
def add_seam(klass, *args)
|
206
|
+
@writers << klass.new(@core_stopper_callback, @write_to_channel, *args)
|
207
|
+
|
208
|
+
@writers.last
|
209
|
+
end
|
210
|
+
|
211
|
+
#---------------------------------------------------------------------------
|
212
|
+
# Protecteds
|
213
|
+
#---------------------------------------------------------------------------
|
214
|
+
protected
|
215
|
+
|
216
|
+
# The block to be called when starting up. Writers should all have been
|
217
|
+
# added before calling this; if writers are started after this, they won't
|
218
|
+
# get the first bytes that are read (due to start-up time).
|
219
|
+
#
|
220
|
+
# This is used internally by child Readers to signal that they're up and
|
221
|
+
# running. If implementing your own Reader, make sure to call this.
|
222
|
+
def starter
|
223
|
+
@starter ||= EM.Callback { @running = true }
|
224
|
+
end
|
225
|
+
|
226
|
+
# The callback that gets called when the Reader is done reading. Tells all
|
227
|
+
# of the associated writers to finish up.
|
228
|
+
#
|
229
|
+
# @return [EventMachine::Callback] The Callback that should get
|
230
|
+
# called by any Reader when it's done reading.
|
231
|
+
def stopper
|
232
|
+
return @stopper if @stopper
|
233
|
+
|
234
|
+
@stopper = EM.Callback do
|
235
|
+
log "Got called back after finished reading. Starting shutdown..."
|
236
|
+
|
237
|
+
# remove this next_tick?
|
238
|
+
EM.next_tick do
|
239
|
+
start_loop = EM.tick_loop do
|
240
|
+
if @writers.empty? || @writers.none?(&:running?)
|
241
|
+
:stop
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
start_loop.on_stop do
|
246
|
+
@running = false
|
247
|
+
puts ">> All done reading on '#{@read_object}'."
|
248
|
+
@core_stopper_callback.call
|
249
|
+
end
|
250
|
+
|
251
|
+
log "Stopping writers for reader #{self.__id__}"
|
252
|
+
EM::Iterator.new(@writers).each do |writer, iter|
|
253
|
+
writer.stop
|
254
|
+
iter.next
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
@stopper
|
260
|
+
end
|
261
|
+
|
262
|
+
#---------------------------------------------------------------------------
|
263
|
+
# Privates
|
264
|
+
#---------------------------------------------------------------------------
|
265
|
+
private
|
266
|
+
|
267
|
+
# Creates a Writer based on the mapping defined in Pants.writers.
|
268
|
+
#
|
269
|
+
# @param [URI] uri The URI that defines the Writer.
|
270
|
+
# @param [EventMachine::Channel] read_from_channel The channel that the
|
271
|
+
# Writer will read from.
|
272
|
+
# @return [Pants::Writer] The newly created Writer.
|
273
|
+
# @raise [ArgumentError] If Pants.writers doesn't contain a mapping for the
|
274
|
+
# URI to a Writer class.
|
275
|
+
def new_writer_from_uri(uri, read_from_channel)
|
276
|
+
writer_to_use = find_writer_from_uri(uri)
|
277
|
+
|
278
|
+
unless writer_to_use
|
279
|
+
raise ArgumentError, "No writer found wth URI scheme: #{uri.scheme}"
|
280
|
+
end
|
281
|
+
|
282
|
+
args = if writer_to_use[:args]
|
283
|
+
writer_to_use[:args].map { |arg| uri.send(arg) }
|
284
|
+
else
|
285
|
+
[]
|
286
|
+
end
|
287
|
+
|
288
|
+
writer_to_use[:klass].new(*args, read_from_channel)
|
289
|
+
end
|
290
|
+
|
291
|
+
# @param [URI] uri The URI that defines the Writer.
|
292
|
+
# @return [Hash] The Hash from Pants.writers that matches the URI.
|
293
|
+
def find_writer_from_uri(uri)
|
294
|
+
if uri.nil?
|
295
|
+
Pants.writers.find { |writer| writer[:uri_scheme].nil? }
|
296
|
+
else
|
297
|
+
Pants.writers.find { |writer| writer[:uri_scheme] == uri.scheme }
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|