listen 0.3.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.4.0 - April 9, 2012
2
+
3
+ ### New features
4
+
5
+ - Add `wait_for_callback` method to all adapters. ([@Maher4Ever][])
6
+ - Add `Listen::MultiListener` class to listen to multiple directories at once. ([@Maher4Ever][])
7
+ - Allow passing multiple directories to the `Listen.to` method. ([@Maher4Ever][])
8
+ - Add `blocking` option to `Listen#start` which can be used to disable blocking the current thread upon starting. ([@Maher4Ever][])
9
+ - Use absolute-paths in callbacks by default instead of relative-paths. ([@Maher4Ever][])
10
+ - Add `relative_paths` option to `Listen::Listener` to retain the old functionality. ([@Maher4Ever][])
11
+
12
+ ### Improvements
13
+
14
+ - Encapsulate thread spawning in the linux-adapter. ([@Maher4Ever][])
15
+ - Encapsulate thread spawning in the darwin-adapter. ([@Maher4Ever][] with [@scottdavis][] help)
16
+ - Encapsulate thread spawning in the windows-adapter. ([@Maher4Ever][])
17
+ - Fix linux-adapter bug where Listen would report file-modification events on the parent-directory. ([@Maher4Ever][])
18
+
19
+ ### Removals
20
+
21
+ - Remove `wait_until_listening` as adapters doesn't need to run inside threads anymore ([@Maher4Ever][])
22
+
1
23
  ## 0.3.3 - March 6, 2012
2
24
 
3
25
  ### Improvements
data/README.md CHANGED
@@ -5,12 +5,13 @@ The Listen gem listens to file modifications and notifies you about the changes.
5
5
  ## Features
6
6
 
7
7
  * Works everywhere!
8
+ * Supports watching multiple directories from a single listener.
8
9
  * OS-specific adapters for Mac OS X 10.6+, Linux and Windows.
9
10
  * Automatic fallback to polling if OS-specific adapter doesn't work.
10
11
  * Detects files modification, addidation and removal.
11
12
  * Checksum comparaison for modifications made under the same second.
13
+ * Allows ignoring paths and supplying filters for better results.
12
14
  * Tested on all Ruby environments via [travis-ci](http://travis-ci.org/guard/listen).
13
- * Threadable.
14
15
 
15
16
  ## Install
16
17
 
@@ -20,19 +21,25 @@ gem install listen
20
21
 
21
22
  ## Usage
22
23
 
23
- There are two ways you can use Listen.
24
+ There are **two ways** to use Listen:
24
25
 
25
- 1. call `Listen.to` with a path params, and define callbacks in a block.
26
- 2. create a `listener` object usable in an (ARel style) chainable way.
26
+ 1. Call `Listen.to` with either a single directory or multiple directories, then define the `change` callback in a block.
27
+ 2. Create a `listener` object and use it in an (ARel style) chainable way.
27
28
 
28
29
  Feel free to give your feeback via [Listen issues](https://github.com/guard/listener/issues)
29
30
 
30
31
  ### Block API
31
32
 
32
33
  ``` ruby
34
+ # Listen to a single directory.
33
35
  Listen.to('dir/path/to/listen', filter: /.*\.rb/, ignore: '/ignored/path') do |modified, added, removed|
34
36
  # ...
35
37
  end
38
+
39
+ # Listen to multiple directories.
40
+ Listen.to('dir/to/awesome_app', 'dir/to/other_app', filter: /.*\.rb/, latency: 0.1) do |modified, added, removed|
41
+ # ...
42
+ end
36
43
  ```
37
44
 
38
45
  ### "Object" API
@@ -45,11 +52,10 @@ listener = listener.latency(0.5)
45
52
  listener = listener.force_polling(true)
46
53
  listener = listener.polling_fallback_message(false)
47
54
  listener = listener.change(&callback)
48
- listener.start # enter the run loop
49
- listener.stop
55
+ listener.start # blocks execution!
50
56
  ```
51
57
 
52
- #### Chainable
58
+ ### Chainable
53
59
 
54
60
  ``` ruby
55
61
  Listen.to('dir/path/to/listen')
@@ -59,21 +65,121 @@ Listen.to('dir/path/to/listen')
59
65
  .force_polling(true)
60
66
  .polling_fallback_message('custom message')
61
67
  .change(&callback)
62
- .start # enter the run loop
68
+ .start # blocks execution!
63
69
  ```
64
70
 
65
- #### Multiple listeners support available via Thread
71
+ ### Pause/Unpause
72
+
73
+ Listener can also easily be paused/unpaused:
66
74
 
67
75
  ``` ruby
68
- listener = Listen.to(dir1).ignore('/ignored/path/')
69
- styles = listener.filter(/.*\.css/).change(&style_callback)
70
- scripts = listener.filter(/.*\.js/).change(&scripts_callback)
76
+ listener = Listen.to('dir/path/to/listen')
77
+ listener.start(false) # non-blocking mode
78
+ listener.pause # stop listening to changes
79
+ listener.paused? # => true
80
+ listener.unpause
81
+ listener.stop
82
+ ```
83
+
84
+ ## Listening to changes on multiple directories
85
+
86
+ The Listen gem provides the `MultiListener` class to watch multiple directories and
87
+ handle their changes from a single listener:
88
+
89
+ ```ruby
90
+ listener = Listen::MultiListener.new('app/css', 'app/js')
91
+ listener.latency(0.5)
92
+
93
+ # Configure the listener to your needs...
94
+
95
+ listener.start # blocks execution!
96
+ ````
97
+
98
+ For an easier access, the `Listen.to` method can also be used to create a multi-listener:
99
+
100
+ ``` ruby
101
+ listener = Listen.to('app/css', 'app/js')
102
+ .ignore('vendor') # both js/vendor and css/vendor will be ignored
103
+ .change(&assets_callback)
104
+
105
+ listener.start # blocks execution!
106
+ ```
107
+
108
+ ## Changes callback
109
+
110
+ Changes to the listened-to directories gets reported back to the user in a callback.
111
+ The registered callback gets invoked, when there are changes, with **three** parameters:
112
+ `modified_paths`, `added_paths` and `removed_paths` in that particular order.
113
+
114
+ You can register a callback in two ways. The first way is by passing a block when calling
115
+ the `Listen.to` method or when initializing a listener object:
116
+
117
+ ```ruby
118
+ Listen.to('path/to/app') do |modified, added, removed|
119
+ # This block will be called when there are changes.
120
+ end
121
+
122
+ # or ...
123
+
124
+ listener = Listen::Listener.new('path/to/app') do |modified, added, removed|
125
+ # This block will be called when there are changes.
126
+ end
127
+
128
+ ```
71
129
 
72
- Thread.new { styles.start } # enter the run loop
73
- Thread.new { scripts.start } # enter the run loop
130
+ The second way to register a callback is be calling the `change` method on any
131
+ listener passing it a block:
132
+
133
+ ```ruby
134
+ # Create a callback
135
+ callback = Proc.new do |modified, added, removed|
136
+ # This proc will be called when there are changes.
137
+ end
138
+
139
+ listener = Listen.to('dir')
140
+ listener.change(&callback) # convert the callback to a block and register it
141
+
142
+ listener.start # blocks execution
143
+ ```
144
+
145
+ ### Paths in callbacks
146
+
147
+ Listeners invoke callbacks passing them absolute paths by default:
148
+
149
+ ```ruby
150
+ # Assume someone changes the 'style.css' file in '/home/user/app/css' after creating
151
+ # the listener.
152
+ Listen.to('/home/user/app/css') do |modified, added, removed|
153
+ modified.inspect # => ['/home/user/app/css/style.css']
154
+ end
155
+ ```
156
+
157
+ #### Relative paths in callbacks
158
+
159
+ When creating a listener for a **single** path (more specifically a `Listen::Listener` instance),
160
+ you can pass `:relative_paths => true` as an option to get relative paths in
161
+ your callback:
162
+
163
+ ```ruby
164
+ # Assume someone changes the 'style.css' file in '/home/user/app/css' after creating
165
+ # the listener.
166
+ Listen.to('/home/user/app/css', :relative_paths => true) do |modified, added, removed|
167
+ modified.inspect # => ['style.css']
168
+ end
74
169
  ```
75
170
 
76
- ### Options
171
+ Passing the `:relative_paths => true` option won't work when listeneing to multiple
172
+ directories:
173
+
174
+ ```ruby
175
+ # Assume someone changes the 'style.css' file in '/home/user/app/css' after creating
176
+ # the listener.
177
+ Listen.to('/home/user/app/css', '/home/user/app/js', :relative_paths => true) do |modified, added, removed|
178
+ modified.inspect # => ['/home/user/app/css/style.css']
179
+ end
180
+ ```
181
+
182
+ ## Options
77
183
 
78
184
  These options can be set through `Listen.to` params or via methods (see the "Object" API)
79
185
 
@@ -94,19 +200,28 @@ These options can be set through `Listen.to` params or via methods (see the "Obj
94
200
  # default: "WARNING: Listen fallen back to polling, learn more at https://github.com/guard/listen#fallback."
95
201
  ```
96
202
 
97
- ### Pause/Unpause
203
+ ### Non-blocking listening to changes
98
204
 
99
- Listener can also easily be paused/unpaused:
205
+ Starting a listener blocks the current thread by default. That means any code after the
206
+ `start` call won't be run until the listener is stopped (which needs to be done from another thread).
100
207
 
101
- ``` ruby
208
+ For advanced usage there is an option to disable this behavior and have the listener start working
209
+ in the background without blocking. To enable non-blocking listening the `start` method of
210
+ the listener (be it `Listener` or `MultiListener`) needs to be called with `false` as a parameter.
211
+
212
+ Here is an example of using a listener in the non-blocking mode:
213
+
214
+ ```ruby
102
215
  listener = Listen.to('dir/path/to/listen')
103
- listener.start # enter the run loop
104
- listener.pause # stop listening changes
105
- listener.paused? => true
106
- listener.unpause
107
- listener.stop
216
+ listener.start(false) # doesn't block execution
217
+
218
+ # Code here will run immediately after starting the listener
219
+
108
220
  ```
109
221
 
222
+ **note**: Using the `Listen.to` helper-method with a callback-block will always
223
+ block execution. See the "Block API" section for an example.
224
+
110
225
  ## Listen adapters
111
226
 
112
227
  The Listen gem has a set of adapters to notify it when there are changes.
@@ -122,17 +237,17 @@ while initializing the listener or call the `force_polling` method on your liste
122
237
  before starting it.
123
238
 
124
239
  <a name="fallback"/>
125
- ### Polling fallback
240
+ ## Polling fallback
126
241
 
127
- When the OS-specific adapter doesn't work the Listen gem automatically falls back to the polling adapter.
128
- Here some things to try to avoiding this fallback:
242
+ When a OS-specific adapter doesn't work the Listen gem automatically falls back to the polling adapter.
243
+ Here are some things you could try to avoid the polling fallback:
129
244
 
130
245
  * [Update your Dropbox client](http://www.dropbox.com/downloading) (if used).
131
246
  * Increase latency. (Please [open an issue](https://github.com/guard/listen/issues/new) if you think that default is too low.)
132
247
  * Move or rename the listened folder.
133
248
  * Update/reboot your OS.
134
249
 
135
- If it still falling back, feel free to [open an issue](https://github.com/guard/listen/issues/new) (and be sure to give all details).
250
+ 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).
136
251
 
137
252
  ## Development [![Dependency Status](https://gemnasium.com/guard/listen.png?branch=master)](https://gemnasium.com/guard/listen)
138
253
 
@@ -159,15 +274,17 @@ For questions please join us in our [Google group](http://groups.google.com/grou
159
274
  * [stereobooster][] for [rb-fchange][], windows support wouldn't exist without him.
160
275
  * [Yehuda Katz (wycats)][] for [vigilo][], that has been a great source of inspiration.
161
276
 
162
- ## Author
277
+ ## Authors
163
278
 
164
- [Thibaud Guillaume-Gentil][] ([@thibaudgg](http://twitter.com/thibaudgg))
279
+ * [Thibaud Guillaume-Gentil][] ([@thibaudgg](http://twitter.com/thibaudgg))
280
+ * [Maher Sallam][] ([@mahersalam](http://twitter.com/mahersalam))
165
281
 
166
282
  ## Contributors
167
283
 
168
284
  [https://github.com/guard/listen/contributors](https://github.com/guard/listen/contributors)
169
285
 
170
286
  [Thibaud Guillaume-Gentil]: https://github.com/thibaudgg
287
+ [Maher Sallam]: https://github.com/Maher4Ever
171
288
  [Michael Kessler (netzpirat)]: https://github.com/netzpirat
172
289
  [Travis Tilley (ttilley)]: https://github.com/ttilley
173
290
  [fssm]: https://github.com/ttilley/fssm
data/lib/listen.rb CHANGED
@@ -1,15 +1,22 @@
1
- require 'listen/listener'
2
-
3
1
  module Listen
4
2
 
5
- # Listen to file system modifications.
3
+ autoload :Turnstile, 'listen/turnstile'
4
+ autoload :Listener, 'listen/listener'
5
+ autoload :MultiListener, 'listen/multi_listener'
6
+ autoload :DirectoryRecord, 'listen/directory_record'
7
+ autoload :Adapter, 'listen/adapter'
8
+
9
+ module Adapters
10
+ autoload :Darwin, 'listen/adapters/darwin'
11
+ autoload :Linux, 'listen/adapters/linux'
12
+ autoload :Windows, 'listen/adapters/windows'
13
+ autoload :Polling, 'listen/adapters/polling'
14
+ end
15
+
16
+ # Listens to filesystem modifications on a either single directory or multiple directories.
6
17
  #
7
- # @param [String, Pathname] dir the directory to watch
8
- # @param [Hash] options the listen options
9
- # @option options [String] ignore a list of paths to ignore
10
- # @option options [Regexp] filter a list of regexps file filters
11
- # @option options [Float] latency the delay between checking for changes in seconds
12
- # @option options [Boolean] polling whether to force or disable the polling adapter
18
+ # @param (see Listen::Listener#new)
19
+ # @param (see Listen::MultiListener#new)
13
20
  #
14
21
  # @yield [modified, added, removed] the changed files
15
22
  # @yieldparam [Array<String>] modified the list of modified files
@@ -19,7 +26,12 @@ module Listen
19
26
  # @return [Listen::Listener] the file listener if no block given
20
27
  #
21
28
  def self.to(*args, &block)
22
- listener = Listener.new(*args, &block)
29
+ listener = if args.length == 1 || ! args[1].is_a?(String)
30
+ Listener.new(*args, &block)
31
+ else
32
+ MultiListener.new(*args, &block)
33
+ end
34
+
23
35
  block ? listener.start : listener
24
36
  end
25
37
 
@@ -1,18 +1,22 @@
1
1
  require 'rbconfig'
2
+ require 'thread'
3
+ require 'set'
4
+ require 'fileutils'
2
5
 
3
6
  module Listen
4
7
  class Adapter
5
- attr_accessor :latency, :paused
8
+ attr_accessor :directories, :latency, :paused
6
9
 
7
10
  # The default delay between checking for changes.
8
11
  DEFAULT_LATENCY = 0.1
12
+
9
13
  # The default warning message when falling back to polling adapter.
10
- POLLING_FALLBACK_MESSAGE = "WARNING: Listen fallen back to polling, learn more at https://github.com/guard/listen#fallback."
14
+ POLLING_FALLBACK_MESSAGE = "WARNING: Listen has fallen back to polling, learn more at https://github.com/guard/listen#fallback."
11
15
 
12
- # Select the appropriate adapter implementation for the
16
+ # Selects the appropriate adapter implementation for the
13
17
  # current OS and initializes it.
14
18
  #
15
- # @param [String, Pathname] directory the directory to watch
19
+ # @param [String, Array<String>] directories the directories to watch
16
20
  # @param [Hash] options the adapter options
17
21
  # @option options [Boolean] force_polling to force polling or not
18
22
  # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
@@ -24,26 +28,26 @@ module Listen
24
28
  #
25
29
  # @return [Listen::Adapter] the chosen adapter
26
30
  #
27
- def self.select_and_initialize(directory, options = {}, &callback)
28
- return Adapters::Polling.new(directory, options, &callback) if options.delete(:force_polling)
31
+ def self.select_and_initialize(directories, options = {}, &callback)
32
+ return Adapters::Polling.new(directories, options, &callback) if options.delete(:force_polling)
29
33
 
30
- if Adapters::Darwin.usable_and_work?(directory, options)
31
- Adapters::Darwin.new(directory, options, &callback)
32
- elsif Adapters::Linux.usable_and_work?(directory, options)
33
- Adapters::Linux.new(directory, options, &callback)
34
- elsif Adapters::Windows.usable_and_work?(directory, options)
35
- Adapters::Windows.new(directory, options, &callback)
34
+ if Adapters::Darwin.usable_and_works?(directories, options)
35
+ Adapters::Darwin.new(directories, options, &callback)
36
+ elsif Adapters::Linux.usable_and_works?(directories, options)
37
+ Adapters::Linux.new(directories, options, &callback)
38
+ elsif Adapters::Windows.usable_and_works?(directories, options)
39
+ Adapters::Windows.new(directories, options, &callback)
36
40
  else
37
41
  unless options[:polling_fallback_message] == false
38
42
  Kernel.warn(options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE)
39
43
  end
40
- Adapters::Polling.new(directory, options, &callback)
44
+ Adapters::Polling.new(directories, options, &callback)
41
45
  end
42
46
  end
43
47
 
44
- # Initialize the adapter.
48
+ # Initializes the adapter.
45
49
  #
46
- # @param [String, Pathname] directory the directory to watch
50
+ # @param [String, Array<String>] directories the directories to watch
47
51
  # @param [Hash] options the adapter options
48
52
  # @option options [Float] latency the delay between checking for changes in seconds
49
53
  #
@@ -53,68 +57,103 @@ module Listen
53
57
  #
54
58
  # @return [Listen::Adapter] the adapter
55
59
  #
56
- def initialize(directory, options = {}, &callback)
57
- @directory = directory
58
- @callback = callback
59
- @latency ||= DEFAULT_LATENCY
60
- @latency = options[:latency] if options[:latency]
61
- @paused = false
60
+ def initialize(directories, options = {}, &callback)
61
+ @directories = Array(directories)
62
+ @callback = callback
63
+ @latency ||= DEFAULT_LATENCY
64
+ @latency = options[:latency] if options[:latency]
65
+ @paused = false
66
+ @mutex = Mutex.new
67
+ @changed_dirs = Set.new
68
+ @turnstile = Turnstile.new
62
69
  end
63
70
 
64
- # Start the adapter.
71
+ # Starts the adapter.
65
72
  #
66
- def start
73
+ # @param [Boolean] blocking whether or not to block the current thread after starting
74
+ #
75
+ def start(blocking = true)
67
76
  @stop = false
68
77
  end
69
78
 
70
- # Stop the adapter.
79
+ # Stops the adapter.
71
80
  #
72
81
  def stop
73
82
  @stop = true
83
+ @turnstile.signal # ensure no thread is blocked
74
84
  end
75
85
 
76
- private
86
+ # Blocks the main thread until the poll thread
87
+ # calls the callback.
88
+ #
89
+ def wait_for_callback
90
+ @turnstile.wait unless @paused
91
+ end
77
92
 
78
- # Check if the adapter is usable and works on the current OS.
93
+ # Checks if the adapter is usable and works on the current OS.
79
94
  #
80
- # @param [String, Pathname] directory the directory to watch
95
+ # @param [String, Array<String>] directories the directories to watch
81
96
  # @param [Hash] options the adapter options
82
97
  # @option options [Float] latency the delay between checking for changes in seconds
83
98
  #
84
99
  # @return [Boolean] whether usable and work or not
85
100
  #
86
- def self.usable_and_work?(directory, options = {})
87
- usable? && work?(directory, options)
101
+ def self.usable_and_works?(directories, options = {})
102
+ usable? && Array(directories).all? { |d| works?(d, options) }
88
103
  end
89
104
 
90
- # Check if the adapter is really working on the current OS by actually testing it.
91
- # This test take some time depending the adapter latency (max latency + 0.2 seconds).
105
+ # Runs a tests to determine if the adapter can actually pick up
106
+ # changes in a given directory and returns the result.
107
+ #
108
+ # @note This test takes some time depending the adapter latency.
92
109
  #
93
110
  # @param [String, Pathname] directory the directory to watch
94
111
  # @param [Hash] options the adapter options
95
112
  # @option options [Float] latency the delay between checking for changes in seconds
96
113
  #
97
- # @return [Boolean] whether work or not
98
- #
99
- def self.work?(directory, options = {})
100
- @work = nil
101
- Thread.new do
102
- begin
103
- callback = lambda { |changed_dirs, options| @work = true }
104
- adapter = self.new(directory, options, &callback)
105
- thread = Thread.new { adapter.start }
106
- sleep 0.1 # wait for the adapter starts
107
- FileUtils.touch "#{directory}/.listen_test"
108
- sleep adapter.latency + 0.1 # wait for callback
109
- @work ||= false
110
- Thread.kill(thread)
111
- ensure
112
- FileUtils.rm "#{directory}/.listen_test"
114
+ # @return [Boolean] whether the adapter works or not
115
+ #
116
+ def self.works?(directory, options = {})
117
+ work = false
118
+ test_file = "#{directory}/.listen_test"
119
+ callback = lambda { |changed_dirs, options| work = true }
120
+ adapter = self.new(directory, options, &callback)
121
+ adapter.start(false)
122
+
123
+ FileUtils.touch(test_file)
124
+
125
+ t = Thread.new { sleep(adapter.latency * 5); adapter.stop }
126
+
127
+ adapter.wait_for_callback
128
+ work
129
+ ensure
130
+ Thread.kill(t) if t
131
+ FileUtils.rm(test_file) if File.exists?(test_file)
132
+ adapter.stop
133
+ end
134
+
135
+ private
136
+
137
+ # Polls changed directories and reports them back
138
+ # when there are changes.
139
+ #
140
+ # @option [Boolean] recursive whether or not to pass the recursive option to the callback
141
+ #
142
+ def poll_changed_dirs(recursive = false)
143
+ until @stop
144
+ sleep(@latency)
145
+ next if @changed_dirs.empty?
146
+
147
+ changed_dirs = []
148
+
149
+ @mutex.synchronize do
150
+ changed_dirs = @changed_dirs.to_a
151
+ @changed_dirs.clear
113
152
  end
153
+
154
+ @callback.call(changed_dirs, recursive ? {:recursive => recursive} : {})
155
+ @turnstile.signal
114
156
  end
115
- sleep 0.01 while @work.nil?
116
- @work
117
157
  end
118
-
119
158
  end
120
159
  end