listen 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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