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