listen 0.3.3 → 0.4.0

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