listen 2.4.1 → 2.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 968f171fec1b3b5600ac4e789f5d01ec91c8acbf
4
- data.tar.gz: 38a66c48a8ea7a1c1a56511349c3372f2dee13a3
3
+ metadata.gz: 042d2544c103c6d5dfbf379cfc2c9f3f6bf84f3e
4
+ data.tar.gz: dc1649706fbc543ea1cce0ef66f004f27f785152
5
5
  SHA512:
6
- metadata.gz: b5ac8813595d071733abef351324858ffb0f2af05a6f25ee7e629ff3e8a5d9863b3c4b7988352790986b9d08e653a152b9c8cf1d7caba9745c319a344b017dca
7
- data.tar.gz: 4e6b618a757f09d028dd48d057e597faf345fa65e258a54b5d7b4f6edc5fd0244b31e8b7979f65dc12f7b9ea2b188f40bdf55e57ca3831627d8ea78aecdc0105
6
+ metadata.gz: d97f0b9f72731fe5675324aee96cc6671efd538bec2207f3b3386c5fb98e3aab1f85251f36b8f8a12447a2932c7ae0dfb9220bb37e3fd369326280037ead5d87
7
+ data.tar.gz: 5facee3fb794d2c49670a783119d434c589da911b12dd4b32445808e8b8750e19513ba57045da4ce6e18c38817e3b31d26bd219bb8b7d402523bb0e66544adba
data/README.md CHANGED
@@ -11,6 +11,7 @@ The Listen gem listens to file modifications and notifies you about the changes.
11
11
  * Detects file modification, addition and removal.
12
12
  * Allows supplying regexp-patterns to ignore paths for better results.
13
13
  * File content checksum comparison for modifications made under the same second (OS X only).
14
+ * Forwarding file events over TCP, [more info](#forwarding-file-events-over-tcp) below.
14
15
  * Tested on MRI Ruby environments (1.9+ only) via [Travis CI](https://travis-ci.org/guard/listen),
15
16
 
16
17
  Please note that:
@@ -192,6 +193,35 @@ Here are some things you could try to avoid forcing polling.
192
193
 
193
194
  If your application keeps using the polling-adapter and you can't figure out why, feel free to [open an issue](https://github.com/guard/listen/issues/new) (and be sure to [give all the details](https://github.com/guard/listen/blob/master/CONTRIBUTING.md)).
194
195
 
196
+ ## Forwarding file events over TCP
197
+
198
+ Listen is capable of forwarding file events over the network using a messaging protocol. This can be useful for virtualized development environments when file events are unavailable, as is the case with [Vagrant](https://github.com/mitchellh/vagrant).
199
+
200
+ To broadcast events over TCP, use the `forward_to` option with an address - just a port or a hostname/port combination:
201
+
202
+ ```ruby
203
+ listener = Listen.to 'path/to/app', forward_to: '10.0.0.2:4000' do |modified, added, removed|
204
+ # After broadcasting the changes to any connected recipients,
205
+ # this block will still be called
206
+ end
207
+ listener.start
208
+ sleep
209
+ ```
210
+
211
+ To connect to a broadcasting listener as a recipient, specify its address using `Listen.on`:
212
+
213
+ ```ruby
214
+ listener = Listen.on '10.0.0.2:4000' do |modified, added, removed|
215
+ # This block will be called
216
+ end
217
+ listener.start
218
+ sleep
219
+ ```
220
+
221
+ ### Security considerations
222
+
223
+ Since file events potentially expose sensitive information, care must be taken when specifying the broadcaster address. It is recommended to **always** specify a hostname and make sure it is as specific as possible to reduce any undesirable eavesdropping.
224
+
195
225
  ## Development
196
226
 
197
227
  * Documentation hosted at [RubyDoc](http://rubydoc.info/github/guard/listen/master/frames).
data/lib/listen.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'celluloid'
2
2
  require 'listen/listener'
3
+ require 'listen/tcp/listener'
3
4
 
4
5
  module Listen
5
6
  class << self
@@ -7,6 +8,8 @@ module Listen
7
8
 
8
9
  # Listens to file system modifications on a either single directory or multiple directories.
9
10
  #
11
+ # When :forward_to is specified, this listener will broadcast modifications over TCP.
12
+ #
10
13
  # @param (see Listen::Listener#new)
11
14
  #
12
15
  # @yield [modified, added, removed] the changed files
@@ -18,7 +21,12 @@ module Listen
18
21
  #
19
22
  def to(*args, &block)
20
23
  @stopping = false
21
- Listener.new(*args, &block)
24
+ options = args.last.is_a?(Hash) ? args.last : {}
25
+ if target = options.delete(:forward_to)
26
+ TCP::Listener.new(target, :broadcaster, *args, &block)
27
+ else
28
+ Listener.new(*args, &block)
29
+ end
22
30
  end
23
31
 
24
32
  # Stop all listeners
@@ -26,5 +34,22 @@ module Listen
26
34
  def stop
27
35
  @stopping = true
28
36
  end
37
+
38
+ # Listens to file system modifications broadcast over TCP.
39
+ #
40
+ # @param [String/Fixnum] target to listen on (hostname:port or port)
41
+ #
42
+ # @yield [modified, added, removed] the changed files
43
+ # @yieldparam [Array<String>] modified the list of modified files
44
+ # @yieldparam [Array<String>] added the list of added files
45
+ # @yieldparam [Array<String>] removed the list of removed files
46
+ #
47
+ # @return [Listen::Listener] the listener
48
+ #
49
+ def on(target, *args, &block)
50
+ TCP::Listener.new(target, :recipient, *args, &block)
51
+ end
52
+
29
53
  end
54
+
30
55
  end
@@ -3,6 +3,7 @@ require 'listen/adapter/bsd'
3
3
  require 'listen/adapter/darwin'
4
4
  require 'listen/adapter/linux'
5
5
  require 'listen/adapter/polling'
6
+ require 'listen/adapter/tcp'
6
7
  require 'listen/adapter/windows'
7
8
 
8
9
  module Listen
@@ -11,6 +12,7 @@ module Listen
11
12
  POLLING_FALLBACK_MESSAGE = "Listen will be polling for changes. Learn more at https://github.com/guard/listen#polling-fallback."
12
13
 
13
14
  def self.select(options = {})
15
+ return TCP if options[:force_tcp]
14
16
  return Polling if options[:force_polling]
15
17
  return _usable_adapter_class if _usable_adapter_class
16
18
 
@@ -0,0 +1,64 @@
1
+ require 'celluloid/io'
2
+
3
+ module Listen
4
+ module Adapter
5
+
6
+ # Adapter to receive file system modifications over TCP
7
+ class TCP < Base
8
+ include Celluloid::IO
9
+
10
+ finalizer :finalize
11
+
12
+ attr_reader :buffer, :socket
13
+
14
+ def self.usable?
15
+ true
16
+ end
17
+
18
+ # Initializes and starts a Celluloid::IO-powered TCP-recipient
19
+ def start
20
+ @socket = TCPSocket.new(listener.host, listener.port)
21
+ @buffer = String.new
22
+ run
23
+ end
24
+
25
+ # Cleans up buffer and socket
26
+ def finalize
27
+ @buffer = nil
28
+ if @socket
29
+ @socket.close
30
+ @socket = nil
31
+ end
32
+ end
33
+
34
+ # Number of bytes to receive at a time
35
+ RECEIVE_WINDOW = 1024
36
+
37
+ # Continuously receive and asynchronously handle data
38
+ def run
39
+ while data = @socket.recv(RECEIVE_WINDOW)
40
+ async.handle_data(data)
41
+ end
42
+ end
43
+
44
+ # Buffers incoming data and handles messages accordingly
45
+ def handle_data(data)
46
+ @buffer << data
47
+ while message = Listen::TCP::Message.from_buffer(@buffer)
48
+ handle_message(message)
49
+ end
50
+ end
51
+
52
+ # Handles incoming message by notifying of path changes
53
+ def handle_message(message)
54
+ message.object.each do |change, paths|
55
+ paths.each do |path|
56
+ _notify_change(path, change: change.to_sym)
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,72 @@
1
+ require 'celluloid/io'
2
+
3
+ module Listen
4
+ module TCP
5
+ class Broadcaster
6
+ include Celluloid::IO
7
+
8
+ finalizer :finalize
9
+
10
+ attr_reader :server, :sockets
11
+
12
+ # Initializes a Celluloid::IO-powered TCP-broadcaster
13
+ #
14
+ # @param [String] host to broadcast on
15
+ # @param [String] port to broadcast on
16
+ #
17
+ # Note: Listens on all addresses when host is nil
18
+ #
19
+ def initialize(host, port)
20
+ @server = TCPServer.new(host, port)
21
+ @sockets = []
22
+ end
23
+
24
+ # Asynchronously start accepting connections
25
+ def start
26
+ async.run
27
+ end
28
+
29
+ # Cleans up sockets and server
30
+ def finalize
31
+ if @server
32
+ @sockets.clear
33
+ @server.close
34
+ @server = nil
35
+ end
36
+ end
37
+
38
+ # Broadcasts given payload to all connected sockets
39
+ def broadcast(payload)
40
+ @sockets.each do |socket|
41
+ unicast(socket, payload)
42
+ end
43
+ end
44
+
45
+ # Unicasts payload to given socket
46
+ #
47
+ # @return [Boolean] whether writing to socket was succesful
48
+ #
49
+ def unicast(socket, payload)
50
+ socket.write(payload)
51
+ true
52
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
53
+ @sockets.delete(socket)
54
+ false
55
+ end
56
+
57
+ # Continuously accept and handle incoming connections
58
+ def run
59
+ while socket = @server.accept
60
+ handle_connection(socket)
61
+ end
62
+ end
63
+
64
+ # Handles incoming socket connection
65
+ def handle_connection(socket)
66
+ @sockets << socket
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,103 @@
1
+ require 'listen/tcp/broadcaster'
2
+ require 'listen/tcp/message'
3
+
4
+ module Listen
5
+ module TCP
6
+ class Listener < Listen::Listener
7
+
8
+ DEFAULT_HOST = 'localhost'
9
+
10
+ attr_reader :host, :mode, :port
11
+
12
+ # Initializes a listener to broadcast or receive modifications over TCP
13
+ #
14
+ # @param [String/Fixnum] target to listen on (hostname:port or port)
15
+ # @param [Symbol] mode (either :broadcaster or :recipient)
16
+ #
17
+ # @param (see Listen::Listener#new)
18
+ #
19
+ def initialize(target, mode, *args, &block)
20
+ self.mode = mode
21
+ self.target = target
22
+
23
+ super *args, &block
24
+ end
25
+
26
+ def broadcaster?
27
+ @mode == :broadcaster
28
+ end
29
+
30
+ def recipient?
31
+ @mode == :recipient
32
+ end
33
+
34
+ # Initializes and starts TCP broadcaster
35
+ def start
36
+ super
37
+ if broadcaster?
38
+ supervisor.add(Broadcaster, as: :broadcaster, args: [host, port])
39
+ registry[:broadcaster].start
40
+ end
41
+ end
42
+
43
+ # Hook to broadcast changes over TCP
44
+ def block
45
+ if broadcaster?
46
+ Proc.new { |modified, added, removed|
47
+
48
+ # Honour paused and stopped states
49
+ next if @paused || @stopping
50
+
51
+ # Broadcast changes as a hash (see Listen::Adapter::TCP#handle_message)
52
+ message = Message.new(modified: modified, added: added, removed: removed)
53
+ registry[:broadcaster].async.broadcast(message.payload)
54
+
55
+ # Invoke the original callback block
56
+ @block.call(modified, added, removed) if @block
57
+ }
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def _init_options(options = {})
66
+ options = super(options)
67
+ options[:force_tcp] = true if recipient?
68
+ options
69
+ end
70
+
71
+ # Sets listener mode
72
+ #
73
+ # @param [Symbol] mode (either :broadcaster or :recipient)
74
+ #
75
+ def mode=(mode)
76
+ unless [:broadcaster, :recipient].include? mode
77
+ raise ArgumentError, 'TCP::Listener requires mode to be either :broadcaster or :recipient'
78
+ end
79
+ @mode = mode
80
+ end
81
+
82
+ # Sets listener target
83
+ #
84
+ # @param [String/Fixnum] target to listen on (hostname:port or port)
85
+ #
86
+ def target=(target)
87
+ unless target
88
+ raise ArgumentError, 'TCP::Listener requires target to be given'
89
+ end
90
+
91
+ @host = DEFAULT_HOST if recipient?
92
+
93
+ if target.is_a? Fixnum
94
+ @port = target
95
+ else
96
+ @host, @port = target.split(':')
97
+ @port = @port.to_i
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+
3
+ module Listen
4
+ module TCP
5
+ class Message
6
+
7
+ attr_reader :body, :object, :payload, :size
8
+
9
+ HEADER_SIZE = 4
10
+ HEADER_FORMAT = 'N'
11
+ PAYLOAD_FORMAT = "#{HEADER_FORMAT}a*"
12
+
13
+ # Initializes a new message
14
+ #
15
+ # @param [Object] object to initialize message with
16
+ #
17
+ def initialize(object = nil)
18
+ self.object = object if object
19
+ end
20
+
21
+ # Generates message size and payload for given object
22
+ def object=(obj)
23
+ @object = obj
24
+ @body = JSON.generate(@object)
25
+ @size = @body.bytesize
26
+ @payload = [@size, @body].pack(PAYLOAD_FORMAT)
27
+ end
28
+
29
+ # Extracts message size and loads object from given payload
30
+ def payload=(payload)
31
+ @payload = payload
32
+ @size, @body = @payload.unpack(PAYLOAD_FORMAT)
33
+ @object = JSON.parse(@body)
34
+ end
35
+
36
+ # Extracts a message from given buffer
37
+ def self.from_buffer(buffer)
38
+ if buffer.bytesize > HEADER_SIZE
39
+ size = buffer.unpack(HEADER_FORMAT).first
40
+ payload_size = HEADER_SIZE + size
41
+ if buffer.bytesize >= payload_size
42
+ payload = buffer.slice!(0...payload_size)
43
+ new.tap do |message|
44
+ message.payload = payload
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module Listen
2
- VERSION = '2.4.1'
2
+ VERSION = '2.5.0'
3
3
  end
data/listen.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.required_ruby_version = ">= 1.9.3"
21
21
 
22
22
  s.add_dependency 'celluloid', '>= 0.15.2'
23
+ s.add_dependency 'celluloid-io', '>= 0.15.0'
23
24
  s.add_dependency 'rb-fsevent', '>= 0.9.3'
24
25
  s.add_dependency 'rb-inotify', '>= 0.9'
25
26
 
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ describe Listen::TCP do
4
+
5
+ let(:port) { 4000 }
6
+
7
+ let(:broadcaster) { Listen.to(Dir.pwd, forward_to: port) }
8
+ let(:recipient) { Listen.on(port) }
9
+ let(:callback) { ->(modified, added, removed) {
10
+ add_changes(:modified, modified)
11
+ add_changes(:added, added)
12
+ add_changes(:removed, removed)
13
+ } }
14
+ let(:paths) { Pathname.new(Dir.pwd) }
15
+
16
+ around { |example| fixtures { |path| example.run } }
17
+
18
+ before do
19
+ broadcaster.start
20
+ end
21
+
22
+ context 'when broadcaster' do
23
+ before do
24
+ broadcaster.block = callback
25
+ end
26
+
27
+ it 'still handles local changes' do
28
+ expect(listen {
29
+ touch 'file.rb'
30
+ }).to eq(
31
+ modified: [],
32
+ added: ['file.rb'],
33
+ removed: []
34
+ )
35
+ end
36
+
37
+ it 'may be paused and unpaused' do
38
+ broadcaster.pause
39
+
40
+ expect(listen {
41
+ touch 'file.rb'
42
+ }).to eq(
43
+ modified: [],
44
+ added: [],
45
+ removed: []
46
+ )
47
+
48
+ broadcaster.unpause
49
+
50
+ expect(listen {
51
+ touch 'file.rb'
52
+ }).to eq(
53
+ modified: ['file.rb'],
54
+ added: [],
55
+ removed: []
56
+ )
57
+ end
58
+
59
+ it 'may be stopped and restarted' do
60
+ broadcaster.stop
61
+
62
+ expect(listen {
63
+ touch 'file.rb'
64
+ }).to eq(
65
+ modified: [],
66
+ added: [],
67
+ removed: []
68
+ )
69
+
70
+ broadcaster.start
71
+
72
+ expect(listen {
73
+ touch 'file.rb'
74
+ }).to eq(
75
+ modified: ['file.rb'],
76
+ added: [],
77
+ removed: []
78
+ )
79
+ end
80
+ end
81
+
82
+ context 'when recipient' do
83
+ before do
84
+ recipient.start
85
+ recipient.block = callback
86
+ end
87
+
88
+ it 'receives changes over TCP' do
89
+ expect(listen {
90
+ touch 'file.rb'
91
+ }).to eq(
92
+ modified: [],
93
+ added: ['file.rb'],
94
+ removed: []
95
+ )
96
+ end
97
+
98
+ it 'may be paused and unpaused' do
99
+ recipient.pause
100
+
101
+ expect(listen {
102
+ touch 'file.rb'
103
+ }).to eq(
104
+ modified: [],
105
+ added: [],
106
+ removed: []
107
+ )
108
+
109
+ recipient.unpause
110
+
111
+ expect(listen {
112
+ touch 'file.rb'
113
+ }).to eq(
114
+ modified: ['file.rb'],
115
+ added: [],
116
+ removed: []
117
+ )
118
+ end
119
+
120
+ it 'may be stopped and restarted' do
121
+ recipient.stop
122
+
123
+ expect(listen {
124
+ touch 'file.rb'
125
+ }).to eq(
126
+ modified: [],
127
+ added: [],
128
+ removed: []
129
+ )
130
+
131
+ recipient.start
132
+
133
+ expect(listen {
134
+ touch 'file.rb'
135
+ }).to eq(
136
+ modified: ['file.rb'],
137
+ added: [],
138
+ removed: []
139
+ )
140
+ end
141
+ end
142
+
143
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe Listen::Adapter::TCP do
4
+
5
+ let(:host) { '10.0.0.2' }
6
+ let(:port) { 4000 }
7
+
8
+ subject { described_class.new(listener) }
9
+ let(:registry) { double(Celluloid::Registry) }
10
+ let(:listener) { double(Listen::TCP::Listener, registry: registry, options: {}, host: host, port: port) }
11
+ let(:socket) { double(described_class::TCPSocket, close: true, recv: nil) }
12
+
13
+ before do
14
+ described_class::TCPSocket.stub(:new).and_return socket
15
+ end
16
+
17
+ after do
18
+ subject.terminate
19
+ end
20
+
21
+ describe '.usable?' do
22
+ it 'always returns true' do
23
+ expect(described_class).to be_usable
24
+ end
25
+ end
26
+
27
+ describe '#start' do
28
+ it 'initializes and exposes a socket with listener host and port' do
29
+ expect(described_class::TCPSocket).to receive(:new).with listener.host, listener.port
30
+ subject.start
31
+ expect(subject.socket).to be socket
32
+ end
33
+
34
+ it 'initializes and exposes a string buffer' do
35
+ subject.start
36
+ expect(subject.buffer).to eq ''
37
+ end
38
+
39
+ it 'invokes run loop' do
40
+ expect(subject.wrapped_object).to receive(:run)
41
+ subject.start
42
+ end
43
+ end
44
+
45
+ describe '#finalize' do
46
+ it 'clears buffer' do
47
+ subject.start
48
+ subject.finalize
49
+ expect(subject.buffer).to be_nil
50
+ end
51
+
52
+ it 'closes socket' do
53
+ subject.start
54
+ expect(subject.socket).to receive(:close)
55
+ subject.finalize
56
+ expect(subject.socket).to be_nil
57
+ end
58
+ end
59
+
60
+ describe '#run' do
61
+ let(:async) { double('TCP-adapter async', handle_data: true) }
62
+
63
+ it 'handles data from socket' do
64
+ socket.stub(:recv).and_return 'foo', 'bar', nil
65
+ subject.stub(:async).and_return async
66
+
67
+ expect(async).to receive(:handle_data).with 'foo'
68
+ expect(async).to receive(:handle_data).with 'bar'
69
+
70
+ subject.start
71
+ end
72
+ end
73
+
74
+ describe '#handle_data' do
75
+ it 'buffers data' do
76
+ subject.start
77
+ subject.handle_data 'foo'
78
+ subject.handle_data 'bar'
79
+ expect(subject.buffer).to eq 'foobar'
80
+ end
81
+
82
+ it 'handles messages accordingly' do
83
+ message = Listen::TCP::Message.new
84
+
85
+ Listen::TCP::Message.stub(:from_buffer).and_return message, nil
86
+ expect(Listen::TCP::Message).to receive(:from_buffer).with 'foo'
87
+ expect(subject.wrapped_object).to receive(:handle_message).with message
88
+
89
+ subject.start
90
+ subject.handle_data 'foo'
91
+ end
92
+ end
93
+
94
+ describe '#handle_message' do
95
+ it 'notifies listener of path changes' do
96
+ message = Listen::TCP::Message.new(
97
+ 'modified' => ['/foo', '/bar'],
98
+ 'added' => ['/baz'],
99
+ 'removed' => []
100
+ )
101
+
102
+ expect(subject.wrapped_object).to receive(:_notify_change).with '/foo', change: :modified
103
+ expect(subject.wrapped_object).to receive(:_notify_change).with '/bar', change: :modified
104
+ expect(subject.wrapped_object).to receive(:_notify_change).with '/baz', change: :added
105
+
106
+ subject.handle_message message
107
+ end
108
+ end
109
+
110
+ end
@@ -10,6 +10,11 @@ describe Listen::Adapter do
10
10
  }
11
11
 
12
12
  describe ".select" do
13
+ it 'returns TCP adapter when requested' do
14
+ klass = Listen::Adapter.select(force_tcp: true)
15
+ expect(klass).to eq Listen::Adapter::TCP
16
+ end
17
+
13
18
  it "returns Polling adapter if forced" do
14
19
  klass = Listen::Adapter.select(force_polling: true)
15
20
  expect(klass).to eq Listen::Adapter::Polling
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ describe Listen::TCP::Broadcaster do
4
+
5
+ let(:host) { '10.0.0.2' }
6
+ let(:port) { 4000 }
7
+
8
+ subject { described_class.new(host, port) }
9
+ let(:server) { double(described_class::TCPServer, close: true, accept: nil) }
10
+ let(:socket) { double(described_class::TCPSocket, write: true) }
11
+ let(:payload) { Listen::TCP::Message.new.payload }
12
+
13
+ before do
14
+ expect(described_class::TCPServer).to receive(:new).with(host, port).and_return server
15
+ end
16
+
17
+ after do
18
+ subject.terminate
19
+ end
20
+
21
+ describe '#initialize' do
22
+ it 'initializes and exposes a server' do
23
+ expect(subject.server).to be server
24
+ end
25
+
26
+ it 'initializes and exposes a list of sockets' do
27
+ expect(subject.sockets).to eq []
28
+ end
29
+ end
30
+
31
+ describe '#start' do
32
+ let(:async) { double('TCP-listener async') }
33
+
34
+ it 'invokes run loop asynchronously' do
35
+ subject.stub(:async).and_return async
36
+ expect(async).to receive(:run)
37
+ subject.start
38
+ end
39
+ end
40
+
41
+ describe '#finalize' do
42
+ it 'clears sockets' do
43
+ expect(subject.sockets).to receive(:clear)
44
+ subject.finalize
45
+ end
46
+
47
+ it 'closes server' do
48
+ expect(subject.server).to receive(:close)
49
+ subject.finalize
50
+ expect(subject.server).to be_nil
51
+ end
52
+ end
53
+
54
+ describe '#broadcast' do
55
+ it 'unicasts to connected sockets' do
56
+ subject.handle_connection socket
57
+ expect(subject.wrapped_object).to receive(:unicast).with socket, payload
58
+ subject.broadcast payload
59
+ end
60
+ end
61
+
62
+ describe '#unicast' do
63
+ before do
64
+ subject.handle_connection socket
65
+ end
66
+
67
+ context 'when succesful' do
68
+ it 'returns true and leaves socket untouched' do
69
+ expect(subject.unicast(socket, payload)).to be_true
70
+ expect(subject.sockets).to include socket
71
+ end
72
+ end
73
+
74
+ context 'on IO errors' do
75
+ it 'returns false and removes socket from list' do
76
+ socket.stub(:write).and_raise IOError
77
+ expect(subject.unicast(socket, payload)).to be_false
78
+ expect(subject.sockets).not_to include socket
79
+ end
80
+ end
81
+
82
+ context 'on connection reset by peer' do
83
+ it 'returns false and removes socket from list' do
84
+ socket.stub(:write).and_raise Errno::ECONNRESET
85
+ expect(subject.unicast(socket, payload)).to be_false
86
+ expect(subject.sockets).not_to include socket
87
+ end
88
+ end
89
+
90
+ context 'on broken pipe' do
91
+ it 'returns false and removes socket from list' do
92
+ socket.stub(:write).and_raise Errno::EPIPE
93
+ expect(subject.unicast(socket, payload)).to be_false
94
+ expect(subject.sockets).not_to include socket
95
+ end
96
+ end
97
+ end
98
+
99
+ describe '#run' do
100
+ it 'handles incoming connections' do
101
+ server.stub(:accept).and_return socket, nil
102
+ expect(subject.wrapped_object).to receive(:handle_connection).with socket
103
+ subject.run
104
+ end
105
+ end
106
+
107
+ describe '#handle_connection' do
108
+ it 'adds socket to list' do
109
+ subject.handle_connection socket
110
+ expect(subject.sockets).to include socket
111
+ end
112
+ end
113
+
114
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Listen::TCP::Listener do
4
+
5
+ let(:host) { '10.0.0.2' }
6
+ let(:port) { 4000 }
7
+
8
+ subject { described_class.new("#{host}:#{port}", :recipient, options) }
9
+ let(:options) { {} }
10
+ let(:registry) { double(Celluloid::Registry, :[]= => true) }
11
+ let(:supervisor) { double(Celluloid::SupervisionGroup, add: true, pool: true) }
12
+ let(:record) { double(Listen::Record, terminate: true, build: true) }
13
+ let(:silencer) { double(Listen::Silencer, terminate: true) }
14
+ let(:adapter) { double(Listen::Adapter::Base) }
15
+ let(:broadcaster) { double(Listen::TCP::Broadcaster) }
16
+ let(:change_pool) { double(Listen::Change, terminate: true) }
17
+ let(:change_pool_async) { double('ChangePoolAsync') }
18
+ before {
19
+ Celluloid::Registry.stub(:new) { registry }
20
+ Celluloid::SupervisionGroup.stub(:run!) { supervisor }
21
+ registry.stub(:[]).with(:silencer) { silencer }
22
+ registry.stub(:[]).with(:adapter) { adapter }
23
+ registry.stub(:[]).with(:record) { record }
24
+ registry.stub(:[]).with(:change_pool) { change_pool }
25
+ registry.stub(:[]).with(:broadcaster) { broadcaster }
26
+ }
27
+
28
+ describe '#initialize' do
29
+ its(:mode) { should be :recipient }
30
+ its(:host) { should eq host }
31
+ its(:port) { should eq port }
32
+
33
+ it 'raises on invalid mode' do
34
+ expect do
35
+ described_class.new(port, :foo)
36
+ end.to raise_error ArgumentError
37
+ end
38
+
39
+ it 'raises on omitted target' do
40
+ expect do
41
+ described_class.new(nil, :recipient)
42
+ end.to raise_error ArgumentError
43
+ end
44
+ end
45
+
46
+ context 'when broadcaster' do
47
+ subject { described_class.new(port, :broadcaster) }
48
+
49
+ it { should be_a_broadcaster }
50
+ it { should_not be_a_recipient }
51
+
52
+ it 'does not force TCP adapter through options' do
53
+ expect(subject.options).not_to include(force_tcp: true)
54
+ end
55
+
56
+ context 'when host is omitted' do
57
+ its(:host) { should be_nil }
58
+ end
59
+
60
+ describe '#start' do
61
+ before do
62
+ adapter.stub_chain(:async, :start)
63
+ broadcaster.stub(:start)
64
+ end
65
+
66
+ it 'registers broadcaster' do
67
+ expect(supervisor).to receive(:add).with(Listen::TCP::Broadcaster, as: :broadcaster, args: [nil, port])
68
+ subject.start
69
+ end
70
+
71
+ it 'starts broadcaster' do
72
+ expect(broadcaster).to receive(:start)
73
+ subject.start
74
+ end
75
+ end
76
+
77
+ describe '#block' do
78
+ let(:async) { double('TCP broadcaster async', broadcast: true) }
79
+ let(:callback) { double(call: true) }
80
+ let(:changes) { {
81
+ modified: ['/foo'],
82
+ added: [],
83
+ removed: []
84
+ }}
85
+
86
+ before do
87
+ broadcaster.stub(:async).and_return async
88
+ end
89
+
90
+ after do
91
+ subject.block.call changes.values
92
+ end
93
+
94
+ context 'when paused' do
95
+ it 'honours paused state and does nothing' do
96
+ subject.pause
97
+ expect(broadcaster).not_to receive(:async)
98
+ expect(callback).not_to receive(:call)
99
+ end
100
+ end
101
+
102
+ context 'when stopped' do
103
+ let(:thread) { double(join: true) }
104
+ before do
105
+ subject.stub(:thread) { thread }
106
+ end
107
+
108
+ it 'honours stopped state and does nothing' do
109
+ subject.stop
110
+ expect(broadcaster).not_to receive(:async)
111
+ expect(callback).not_to receive(:call)
112
+ end
113
+ end
114
+
115
+ it 'broadcasts changes asynchronously' do
116
+ message = Listen::TCP::Message.new changes
117
+ expect(async).to receive(:broadcast).with message.payload
118
+ end
119
+
120
+ it 'invokes original callback block' do
121
+ subject.block = callback
122
+ expect(callback).to receive(:call).with *changes.values
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'when recipient' do
128
+ subject { described_class.new(port, :recipient) }
129
+
130
+ it 'forces TCP adapter through options' do
131
+ expect(subject.options).to include(force_tcp: true)
132
+ end
133
+
134
+ it { should_not be_a_broadcaster }
135
+ it { should be_a_recipient }
136
+
137
+ context 'when host is omitted' do
138
+ its(:host) { should eq described_class::DEFAULT_HOST }
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Listen::TCP::Message do
6
+
7
+ let(:object) { [1, 2, {'foo' => 'bar'}] }
8
+ let(:body) { '[1,2,{"foo":"bar"}]' }
9
+ let(:size) { 19 }
10
+ let(:payload) { "\x00\x00\x00\x13[1,2,{\"foo\":\"bar\"}]" }
11
+
12
+ describe '#initialize' do
13
+ it 'initializes with an object' do
14
+ message = described_class.new(object)
15
+ expect(message.object).to be object
16
+ end
17
+ end
18
+
19
+ describe '#object=' do
20
+ before do
21
+ subject.object = object
22
+ end
23
+
24
+ its(:object) { should be object }
25
+ its(:body) { should eq body }
26
+ its(:size) { should eq size }
27
+ its(:payload) { should eq payload }
28
+ end
29
+
30
+ describe '#payload=' do
31
+ before do
32
+ subject.payload = payload
33
+ end
34
+
35
+ its(:object) { should eq object }
36
+ its(:body) { should eq body }
37
+ its(:size) { should eq size }
38
+ its(:payload) { should be payload }
39
+ end
40
+
41
+ describe '.from_buffer' do
42
+
43
+ context 'when buffer is empty' do
44
+ it 'returns nil and leaves buffer intact' do
45
+ buffer = ''
46
+ message = described_class.from_buffer buffer
47
+ expect(message).to be_nil
48
+ expect(buffer).to eq ''
49
+ end
50
+ end
51
+
52
+ context 'when buffer has data' do
53
+
54
+ context 'with a partial packet' do
55
+ it 'returns nil and leaves remaining data intact' do
56
+ buffer = payload[0..4]
57
+ message = described_class.from_buffer buffer
58
+ expect(message).to be_nil
59
+ expect(buffer).to eq payload[0..4]
60
+ end
61
+ end
62
+
63
+ context 'with a full packet' do
64
+ it 'extracts message from buffer and depletes buffer' do
65
+ buffer = payload.dup
66
+ message = described_class.from_buffer buffer
67
+ expect(message).to be_a described_class
68
+ expect(message.object).to eq object
69
+ expect(buffer).to eq ''
70
+ end
71
+ end
72
+
73
+ context 'with a full and a partial packet' do
74
+ it 'extracts message from buffer and leaves remaining data intact' do
75
+ buffer = payload + payload[0..10]
76
+ message = described_class.from_buffer buffer
77
+ expect(message).to be_a described_class
78
+ expect(message.object).to eq object
79
+ expect(buffer).to eq payload[0..10]
80
+ end
81
+ end
82
+
83
+ context 'with two full packets' do
84
+ it 'extracts both messages from buffer and depletes buffer' do
85
+ buffer = payload + payload
86
+
87
+ message1 = described_class.from_buffer buffer
88
+ expect(message1).to be_a described_class
89
+ expect(message1.object).to eq object
90
+
91
+ message2 = described_class.from_buffer buffer
92
+ expect(message2).to be_a described_class
93
+ expect(message2.object).to eq object
94
+
95
+ expect(message1).not_to be message2
96
+ expect(buffer).to eq ''
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -7,6 +7,13 @@ describe Listen do
7
7
  described_class.to('/path')
8
8
  end
9
9
 
10
+ context 'when using :forward_to option' do
11
+ it 'initializes TCP-listener in broadcast-mode' do
12
+ expect(Listen::TCP::Listener).to receive(:new).with(4000, :broadcaster, '/path', {})
13
+ described_class.to('/path', forward_to: 4000)
14
+ end
15
+ end
16
+
10
17
  it "sets stopping at false" do
11
18
  allow(Listen::Listener).to receive(:new)
12
19
  Listen.to('/path')
@@ -20,4 +27,11 @@ describe Listen do
20
27
  expect(Listen.stopping).to be_true
21
28
  end
22
29
  end
30
+
31
+ describe '.on' do
32
+ it 'initializes TCP-listener in recipient-mode' do
33
+ expect(Listen::TCP::Listener).to receive(:new).with(4000, :recipient, '/path')
34
+ described_class.on(4000, '/path')
35
+ end
36
+ end
23
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: listen
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thibaud Guillaume-Gentil
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-07 00:00:00.000000000 Z
11
+ date: 2014-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: celluloid
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.15.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: celluloid-io
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.15.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.15.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rb-fsevent
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -133,6 +147,7 @@ files:
133
147
  - lib/listen/adapter/darwin.rb
134
148
  - lib/listen/adapter/linux.rb
135
149
  - lib/listen/adapter/polling.rb
150
+ - lib/listen/adapter/tcp.rb
136
151
  - lib/listen/adapter/windows.rb
137
152
  - lib/listen/change.rb
138
153
  - lib/listen/directory.rb
@@ -140,14 +155,19 @@ files:
140
155
  - lib/listen/listener.rb
141
156
  - lib/listen/record.rb
142
157
  - lib/listen/silencer.rb
158
+ - lib/listen/tcp/broadcaster.rb
159
+ - lib/listen/tcp/listener.rb
160
+ - lib/listen/tcp/message.rb
143
161
  - lib/listen/version.rb
144
162
  - listen.gemspec
145
163
  - spec/acceptance/listen_spec.rb
164
+ - spec/acceptance/tcp_spec.rb
146
165
  - spec/lib/listen/adapter/base_spec.rb
147
166
  - spec/lib/listen/adapter/bsd_spec.rb
148
167
  - spec/lib/listen/adapter/darwin_spec.rb
149
168
  - spec/lib/listen/adapter/linux_spec.rb
150
169
  - spec/lib/listen/adapter/polling_spec.rb
170
+ - spec/lib/listen/adapter/tcp_spec.rb
151
171
  - spec/lib/listen/adapter/windows_spec.rb
152
172
  - spec/lib/listen/adapter_spec.rb
153
173
  - spec/lib/listen/change_spec.rb
@@ -156,6 +176,9 @@ files:
156
176
  - spec/lib/listen/listener_spec.rb
157
177
  - spec/lib/listen/record_spec.rb
158
178
  - spec/lib/listen/silencer_spec.rb
179
+ - spec/lib/listen/tcp/broadcaster_spec.rb
180
+ - spec/lib/listen/tcp/listener_spec.rb
181
+ - spec/lib/listen/tcp/message_spec.rb
159
182
  - spec/lib/listen_spec.rb
160
183
  - spec/spec_helper.rb
161
184
  - spec/support/acceptance_helper.rb
@@ -187,11 +210,13 @@ specification_version: 4
187
210
  summary: Listen to file modifications
188
211
  test_files:
189
212
  - spec/acceptance/listen_spec.rb
213
+ - spec/acceptance/tcp_spec.rb
190
214
  - spec/lib/listen/adapter/base_spec.rb
191
215
  - spec/lib/listen/adapter/bsd_spec.rb
192
216
  - spec/lib/listen/adapter/darwin_spec.rb
193
217
  - spec/lib/listen/adapter/linux_spec.rb
194
218
  - spec/lib/listen/adapter/polling_spec.rb
219
+ - spec/lib/listen/adapter/tcp_spec.rb
195
220
  - spec/lib/listen/adapter/windows_spec.rb
196
221
  - spec/lib/listen/adapter_spec.rb
197
222
  - spec/lib/listen/change_spec.rb
@@ -200,6 +225,9 @@ test_files:
200
225
  - spec/lib/listen/listener_spec.rb
201
226
  - spec/lib/listen/record_spec.rb
202
227
  - spec/lib/listen/silencer_spec.rb
228
+ - spec/lib/listen/tcp/broadcaster_spec.rb
229
+ - spec/lib/listen/tcp/listener_spec.rb
230
+ - spec/lib/listen/tcp/message_spec.rb
203
231
  - spec/lib/listen_spec.rb
204
232
  - spec/spec_helper.rb
205
233
  - spec/support/acceptance_helper.rb