guard 0.8.4 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/guard/hook.rb CHANGED
@@ -1,118 +1,118 @@
1
- module Guard
2
-
3
- # Guard has a hook mechanism that allows you to insert callbacks for individual Guards.
4
- # By default, each of the Guard instance methods has a "_begin" and an "_end" hook.
5
- # For example, the Guard::Guard#start method has a :start_begin hook that is runs immediately
6
- # before Guard::Guard#start, and a :start_end hook that runs immediately after Guard::Guard#start.
7
- #
8
- # Read more about [hooks and callbacks on the wiki](https://github.com/guard/guard/wiki/Hooks-and-callbacks).
9
- #
10
- module Hook
11
-
12
- # The Hook module gets included.
13
- #
14
- # @param [Class] base the class that includes the module
15
- #
16
- def self.included(base)
17
- base.send :include, InstanceMethods
18
- end
19
-
20
- # Instance methods that gets included in the base class.
21
- #
22
- module InstanceMethods
23
-
24
- # When event is a Symbol, {#hook} will generate a hook name
25
- # by concatenating the method name from where {#hook} is called
26
- # with the given Symbol.
27
- #
28
- # @example Add a hook with a Symbol
29
- #
30
- # def run_all
31
- # hook :foo
32
- # end
33
- #
34
- # Here, when {Guard::Guard#run_all} is called, {#hook} will notify callbacks
35
- # registered for the "run_all_foo" event.
36
- #
37
- # When event is a String, {#hook} will directly turn the String
38
- # into a Symbol.
39
- #
40
- # @example Add a hook with a String
41
- #
42
- # def run_all
43
- # hook "foo_bar"
44
- # end
45
- #
46
- # When {Guard::Guard#run_all} is called, {#hook} will notify callbacks
47
- # registered for the "foo_bar" event.
48
- #
49
- # @param [Symbol, String] event the name of the Guard event
50
- # @param [Array] args the parameters are passed as is to the callbacks registered for the given event.
51
- #
52
- def hook(event, *args)
53
- hook_name = if event.is_a? Symbol
54
- calling_method = caller[0][/`([^']*)'/, 1]
55
- "#{ calling_method }_#{ event }"
56
- else
57
- event
58
- end.to_sym
59
-
60
- UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
61
-
62
- Hook.notify(self.class, hook_name, *args)
63
- end
64
- end
65
-
66
- class << self
67
-
68
- # Get all callbacks.
69
- #
70
- def callbacks
71
- @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
72
- end
73
-
74
- # Add a callback.
75
- #
76
- # @param [Block] listener the listener to notify
77
- # @param [Guard::Guard] guard_class the Guard class to add the callback
78
- # @param [Array<Symbol>] events the events to register
79
- #
80
- def add_callback(listener, guard_class, events)
81
- _events = events.is_a?(Array) ? events : [events]
82
- _events.each do |event|
83
- callbacks[[guard_class, event]] << listener
84
- end
85
- end
86
-
87
- # Checks if a callback has been registered.
88
- #
89
- # @param [Block] listener the listener to notify
90
- # @param [Guard::Guard] guard_class the Guard class to add the callback
91
- # @param [Symbol] event the event to look for
92
- #
93
- def has_callback?(listener, guard_class, event)
94
- callbacks[[guard_class, event]].include?(listener)
95
- end
96
-
97
- # Notify a callback.
98
- #
99
- # @param [Guard::Guard] guard_class the Guard class to add the callback
100
- # @param [Symbol] event the event to trigger
101
- # @param [Array] args the arguments for the listener
102
- #
103
- def notify(guard_class, event, *args)
104
- callbacks[[guard_class, event]].each do |listener|
105
- listener.call(guard_class, event, *args)
106
- end
107
- end
108
-
109
- # Reset all callbacks.
110
- #
111
- def reset_callbacks!
112
- @callbacks = nil
113
- end
114
-
115
- end
116
-
117
- end
118
- end
1
+ module Guard
2
+
3
+ # Guard has a hook mechanism that allows you to insert callbacks for individual Guards.
4
+ # By default, each of the Guard instance methods has a "_begin" and an "_end" hook.
5
+ # For example, the Guard::Guard#start method has a :start_begin hook that is runs immediately
6
+ # before Guard::Guard#start, and a :start_end hook that runs immediately after Guard::Guard#start.
7
+ #
8
+ # Read more about [hooks and callbacks on the wiki](https://github.com/guard/guard/wiki/Hooks-and-callbacks).
9
+ #
10
+ module Hook
11
+
12
+ # The Hook module gets included.
13
+ #
14
+ # @param [Class] base the class that includes the module
15
+ #
16
+ def self.included(base)
17
+ base.send :include, InstanceMethods
18
+ end
19
+
20
+ # Instance methods that gets included in the base class.
21
+ #
22
+ module InstanceMethods
23
+
24
+ # When event is a Symbol, {#hook} will generate a hook name
25
+ # by concatenating the method name from where {#hook} is called
26
+ # with the given Symbol.
27
+ #
28
+ # @example Add a hook with a Symbol
29
+ #
30
+ # def run_all
31
+ # hook :foo
32
+ # end
33
+ #
34
+ # Here, when {Guard::Guard#run_all} is called, {#hook} will notify callbacks
35
+ # registered for the "run_all_foo" event.
36
+ #
37
+ # When event is a String, {#hook} will directly turn the String
38
+ # into a Symbol.
39
+ #
40
+ # @example Add a hook with a String
41
+ #
42
+ # def run_all
43
+ # hook "foo_bar"
44
+ # end
45
+ #
46
+ # When {Guard::Guard#run_all} is called, {#hook} will notify callbacks
47
+ # registered for the "foo_bar" event.
48
+ #
49
+ # @param [Symbol, String] event the name of the Guard event
50
+ # @param [Array] args the parameters are passed as is to the callbacks registered for the given event.
51
+ #
52
+ def hook(event, *args)
53
+ hook_name = if event.is_a? Symbol
54
+ calling_method = caller[0][/`([^']*)'/, 1]
55
+ "#{ calling_method }_#{ event }"
56
+ else
57
+ event
58
+ end.to_sym
59
+
60
+ UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
61
+
62
+ Hook.notify(self.class, hook_name, *args)
63
+ end
64
+ end
65
+
66
+ class << self
67
+
68
+ # Get all callbacks.
69
+ #
70
+ def callbacks
71
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
72
+ end
73
+
74
+ # Add a callback.
75
+ #
76
+ # @param [Block] listener the listener to notify
77
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
78
+ # @param [Array<Symbol>] events the events to register
79
+ #
80
+ def add_callback(listener, guard_class, events)
81
+ _events = events.is_a?(Array) ? events : [events]
82
+ _events.each do |event|
83
+ callbacks[[guard_class, event]] << listener
84
+ end
85
+ end
86
+
87
+ # Checks if a callback has been registered.
88
+ #
89
+ # @param [Block] listener the listener to notify
90
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
91
+ # @param [Symbol] event the event to look for
92
+ #
93
+ def has_callback?(listener, guard_class, event)
94
+ callbacks[[guard_class, event]].include?(listener)
95
+ end
96
+
97
+ # Notify a callback.
98
+ #
99
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
100
+ # @param [Symbol] event the event to trigger
101
+ # @param [Array] args the arguments for the listener
102
+ #
103
+ def notify(guard_class, event, *args)
104
+ callbacks[[guard_class, event]].each do |listener|
105
+ listener.call(guard_class, event, *args)
106
+ end
107
+ end
108
+
109
+ # Reset all callbacks.
110
+ #
111
+ def reset_callbacks!
112
+ @callbacks = nil
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
@@ -1,44 +1,110 @@
1
- module Guard
2
-
3
- # The interactor reads user input and triggers
4
- # specific action upon them unless its locked.
5
- #
6
- # Currently the following actions are implemented:
7
- #
8
- # - stop, quit, exit, s, q, e => Exit Guard
9
- # - reload, r, z => Reload Guard
10
- # - pause, p => Pause Guard
11
- # - Everything else => Run all
12
- #
13
- class Interactor
14
- # Start the interactor in its own thread.
15
- #
16
- def start
17
- return if ENV["GUARD_ENV"] == 'test'
18
-
19
- if !@thread || @thread.stop?
20
- @thread = Thread.new do
21
- while entry = $stdin.gets.chomp
22
- case entry
23
- when 'stop', 'quit', 'exit', 's', 'q', 'e'
24
- ::Guard.stop
25
- when 'reload', 'r', 'z'
26
- ::Guard::Dsl.reevaluate_guardfile
27
- ::Guard.reload
28
- when 'pause', 'p'
29
- ::Guard.pause
30
- else
31
- ::Guard.run_all
32
- end
33
- end
34
- end
35
- end
36
- end
37
-
38
- def stop_if_not_current
39
- unless Thread.current == @thread
40
- @thread.kill
41
- end
42
- end
43
- end
44
- end
1
+ module Guard
2
+
3
+ # The interactor reads user input and triggers
4
+ # specific action upon them unless its locked.
5
+ #
6
+ # Currently the following actions are implemented:
7
+ #
8
+ # - stop, quit, exit, s, q, e => Exit Guard
9
+ # - reload, r, z => Reload Guard
10
+ # - pause, p => Pause Guard
11
+ # - Everything else => Run all
12
+ #
13
+ # It's also possible to scope `reload` and `run all` actions to only a specified group or a guard.
14
+ #
15
+ # @example `backend reload` will only reload backend group
16
+ # @example `spork reload` will only reload rspec guard
17
+ # @example `jasmine` will only run all jasmine specs
18
+ #
19
+ class Interactor
20
+
21
+ STOP_ACTIONS = %w[stop quit exit s q e]
22
+ RELOAD_ACTIONS = %w[reload r z]
23
+ PAUSE_ACTIONS = %w[pause p]
24
+
25
+ # Start the interactor in its own thread.
26
+ #
27
+ def start
28
+ return if ENV["GUARD_ENV"] == 'test'
29
+
30
+ if !@thread || @thread.stop?
31
+ @thread = Thread.new do
32
+ while entry = $stdin.gets.chomp
33
+ scopes, action = extract_scopes_and_action(entry)
34
+ case action
35
+ when :stop
36
+ ::Guard.stop
37
+ when :pause
38
+ ::Guard.pause
39
+ when :reload
40
+ ::Guard::Dsl.reevaluate_guardfile if scopes.empty?
41
+ ::Guard.reload(scopes)
42
+ when :run_all
43
+ ::Guard.run_all(scopes)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Extract guard or group scope and action from Interactor entry
51
+ #
52
+ # @example `spork reload` will only reload rspec
53
+ # @example `jasmine` will only run all jasmine specs
54
+ #
55
+ # @param [String] Interactor entry gets from $stdin
56
+ # @return [Array] entry group or guard scope hash and action
57
+ def extract_scopes_and_action(entry)
58
+ scopes = {}
59
+ entries = entry.split(' ')
60
+ case entries.length
61
+ when 1
62
+ unless action = action_from_entry(entries[0])
63
+ scopes = scopes_from_entry(entries[0])
64
+ end
65
+ when 2
66
+ scopes = scopes_from_entry(entries[0])
67
+ action = action_from_entry(entries[1])
68
+ end
69
+ action ||= :run_all
70
+ [scopes, action]
71
+ end
72
+
73
+ # Extract guard or group scope from entry if valid
74
+ #
75
+ # @param [String] Interactor entry gets from $stdin
76
+ # @return [Hash] An hash with a guard or a group scope
77
+ def scopes_from_entry(entry)
78
+ scopes = {}
79
+ if guard = ::Guard.guards(entry)
80
+ scopes[:guard] = guard
81
+ end
82
+ if group = ::Guard.groups(entry)
83
+ scopes[:group] = group
84
+ end
85
+ scopes
86
+ end
87
+
88
+ # Extract action from entry if an existing action is present
89
+ #
90
+ # @param [String] Interactor entry gets from $stdin
91
+ # @return [Symbol] A guard action
92
+ def action_from_entry(entry)
93
+ if STOP_ACTIONS.include?(entry)
94
+ :stop
95
+ elsif RELOAD_ACTIONS.include?(entry)
96
+ :reload
97
+ elsif PAUSE_ACTIONS.include?(entry)
98
+ :pause
99
+ end
100
+ end
101
+
102
+ # Kill interactor thread if not current
103
+ #
104
+ def stop_if_not_current
105
+ unless Thread.current == @thread
106
+ @thread.kill
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,350 +1,350 @@
1
- require 'rbconfig'
2
- require 'digest/sha1'
3
-
4
- module Guard
5
-
6
- autoload :Darwin, 'guard/listeners/darwin'
7
- autoload :Linux, 'guard/listeners/linux'
8
- autoload :Windows, 'guard/listeners/windows'
9
- autoload :Polling, 'guard/listeners/polling'
10
-
11
- # The Listener is the base class for all listener
12
- # implementations.
13
- #
14
- # @abstract
15
- #
16
- class Listener
17
-
18
- # Default paths that gets ignored by the listener
19
- DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor]
20
-
21
- attr_accessor :changed_files
22
- attr_reader :directory, :ignore_paths
23
-
24
- def paused?
25
- @paused
26
- end
27
-
28
- # Select the appropriate listener implementation for the
29
- # current OS and initializes it.
30
- #
31
- # @param [Array] args the arguments for the listener
32
- # @return [Guard::Listener] the chosen listener
33
- #
34
- def self.select_and_init(*args)
35
- if mac? && Darwin.usable?
36
- Darwin.new(*args)
37
- elsif linux? && Linux.usable?
38
- Linux.new(*args)
39
- elsif windows? && Windows.usable?
40
- Windows.new(*args)
41
- else
42
- UI.info 'Using polling (Please help us to support your system better than that).'
43
- Polling.new(*args)
44
- end
45
- end
46
-
47
- # Initialize the listener.
48
- #
49
- # @param [String] directory the root directory to listen to
50
- # @option options [Boolean] relativize_paths use only relative paths
51
- # @option options [Array<String>] ignore_paths the paths to ignore by the listener
52
- #
53
- def initialize(directory = Dir.pwd, options = {})
54
- @directory = directory.to_s
55
- @sha1_checksums_hash = {}
56
- @file_timestamp_hash = {}
57
- @relativize_paths = options.fetch(:relativize_paths, true)
58
- @changed_files = []
59
- @paused = false
60
- @ignore_paths = DEFAULT_IGNORE_PATHS
61
- @ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
62
- @watch_all_modifications = options.fetch(:watch_all_modifications, false)
63
-
64
- update_last_event
65
- start_reactor
66
- end
67
-
68
- # Start the listener thread.
69
- #
70
- def start_reactor
71
- return if ENV["GUARD_ENV"] == 'test'
72
-
73
- Thread.new do
74
- loop do
75
- if @changed_files != [] && !@paused
76
- changed_files = @changed_files.dup
77
- clear_changed_files
78
- ::Guard.run_on_change(changed_files)
79
- else
80
- sleep 0.1
81
- end
82
- end
83
- end
84
- end
85
-
86
- # Start watching the root directory.
87
- #
88
- def start
89
- watch(@directory)
90
- timestamp_files
91
- end
92
-
93
- # Stop listening for events.
94
- #
95
- def stop
96
- end
97
-
98
- # Pause the listener to ignore change events.
99
- #
100
- def pause
101
- @paused = true
102
- end
103
-
104
- # Unpause the listener to listen again to change events.
105
- #
106
- def run
107
- @paused = false
108
- end
109
-
110
- # Clear the list of changed files.
111
- #
112
- def clear_changed_files
113
- @changed_files.clear
114
- end
115
-
116
- # Store a listener callback.
117
- #
118
- # @param [Block] callback the callback to store
119
- #
120
- def on_change(&callback)
121
- @callback = callback
122
- end
123
-
124
- # Updates the timestamp of the last event.
125
- #
126
- def update_last_event
127
- @last_event = Time.now
128
- end
129
-
130
- # Get the modified files.
131
- #
132
- # If the `:watch_all_modifications` option is true, then moved and
133
- # deleted files are also reported, but prefixed by an exclamation point.
134
- #
135
- # @example Deleted or moved file
136
- # !/home/user/dir/file.rb
137
- #
138
- # @param [Array<String>] dirs the watched directories
139
- # @param [Hash] options the listener options
140
- # @option options [Symbol] all whether to files in sub directories
141
- # @return [Array<String>] paths of files that have been modified
142
- #
143
- def modified_files(dirs, options = {})
144
- last_event = @last_event
145
- files = []
146
- if @watch_all_modifications
147
- deleted_files = @file_timestamp_hash.collect do |path, ts|
148
- unless File.exists?(path)
149
- @sha1_checksums_hash.delete(path)
150
- @file_timestamp_hash.delete(path)
151
- "!#{path}"
152
- end
153
- end
154
- files.concat(deleted_files.compact)
155
- end
156
- update_last_event
157
- files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
158
-
159
- relativize_paths(files)
160
- end
161
-
162
- # Register a directory to watch.
163
- # Must be implemented by the subclasses.
164
- #
165
- # @param [String] directory the directory to watch
166
- #
167
- def watch(directory)
168
- raise NotImplementedError, "do whatever you want here, given the directory as only argument"
169
- end
170
-
171
- # Get all files that are in the watched directory.
172
- #
173
- # @return [Array<String>] the list of files
174
- #
175
- def all_files
176
- potentially_modified_files([@directory], :all => true)
177
- end
178
-
179
- # Scopes all given paths to the current directory.
180
- #
181
- # @param [Array<String>] paths the paths to change
182
- # @return [Array<String>] all paths now relative to the current dir
183
- #
184
- def relativize_paths(paths)
185
- return paths unless relativize_paths?
186
- paths.map do |path|
187
- path.gsub(%r{^(!)?#{ @directory }/},'\1')
188
- end
189
- end
190
-
191
- # Use paths relative to the current directory.
192
- #
193
- # @return [Boolean] whether to use relative or absolute paths
194
- #
195
- def relativize_paths?
196
- !!@relativize_paths
197
- end
198
-
199
- # Populate initial timestamp file hash to watch for deleted or moved files.
200
- #
201
- def timestamp_files
202
- all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
203
- end
204
-
205
- # Removes the ignored paths from the directory list.
206
- #
207
- # @param [Array<String>] dirs the directory to listen to
208
- # @param [Array<String>] ignore_paths the paths to ignore
209
- # @return children of the passed dirs that are not in the ignore_paths list
210
- #
211
- def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
212
- Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
213
- ignore_paths.include?(File.basename(path))
214
- end
215
- end
216
-
217
- private
218
-
219
- # Gets a list of files that are in the modified directories.
220
- #
221
- # @param [Array<String>] dirs the list of directories
222
- # @param [Hash] options the find file option
223
- # @option options [Symbol] all whether to files in sub directories
224
- #
225
- def potentially_modified_files(dirs, options = {})
226
- paths = exclude_ignored_paths(dirs)
227
-
228
- if options[:all]
229
- paths.inject([]) do |array, path|
230
- if File.file?(path)
231
- array << path
232
- else
233
- array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
234
- end
235
- array
236
- end
237
- else
238
- paths.select { |path| File.file?(path) }
239
- end
240
- end
241
-
242
- # Test if the file content has changed.
243
- #
244
- # Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
245
- # both values down to the second for the comparison.
246
- #
247
- # ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
248
- #
249
- # @param [String] path the file path
250
- # @param [Time] last_event the time of the last event
251
- # @return [Boolean] Whether the file content has changed or not.
252
- #
253
- def file_modified?(path, last_event)
254
- ctime = File.ctime(path).to_i
255
- mtime = File.mtime(path).to_i
256
- if [mtime, ctime].max == last_event.to_i
257
- file_content_modified?(path, sha1_checksum(path))
258
- elsif mtime > last_event.to_i
259
- set_sha1_checksums_hash(path, sha1_checksum(path))
260
- true
261
- elsif @watch_all_modifications
262
- ts = file_timestamp(path)
263
- if ts != @file_timestamp_hash[path]
264
- set_file_timestamp_hash(path, ts)
265
- true
266
- end
267
- else
268
- false
269
- end
270
- rescue
271
- false
272
- end
273
-
274
- # Tests if the file content has been modified by
275
- # comparing the SHA1 checksum.
276
- #
277
- # @param [String] path the file path
278
- # @param [String] sha1_checksum the checksum of the file
279
- #
280
- def file_content_modified?(path, sha1_checksum)
281
- if @sha1_checksums_hash[path] != sha1_checksum
282
- set_sha1_checksums_hash(path, sha1_checksum)
283
- true
284
- else
285
- false
286
- end
287
- end
288
-
289
- # Set save a files current timestamp
290
- #
291
- # @param [String] path the file path
292
- # @param [Int] file_timestamp the files modified timestamp
293
- #
294
- def set_file_timestamp_hash(path, file_timestamp)
295
- @file_timestamp_hash[path] = file_timestamp
296
- end
297
-
298
- # Set the current checksum of a file.
299
- #
300
- # @param [String] path the file path
301
- # @param [String] sha1_checksum the checksum of the file
302
- #
303
- def set_sha1_checksums_hash(path, sha1_checksum)
304
- @sha1_checksums_hash[path] = sha1_checksum
305
- end
306
-
307
- # Gets a files modified timestamp
308
- #
309
- # @path [String] path the file path
310
- # @return [Int] file modified timestamp
311
- #
312
- def file_timestamp(path)
313
- File.mtime(path).to_i
314
- end
315
-
316
- # Calculates the SHA1 checksum of a file.
317
- #
318
- # @param [String] path the path to the file
319
- # @return [String] the SHA1 checksum
320
- #
321
- def sha1_checksum(path)
322
- Digest::SHA1.file(path).to_s
323
- end
324
-
325
- # Test if the OS is Mac OS X.
326
- #
327
- # @return [Boolean] Whether the OS is Mac OS X
328
- #
329
- def self.mac?
330
- RbConfig::CONFIG['target_os'] =~ /darwin/i
331
- end
332
-
333
- # Test if the OS is Linux.
334
- #
335
- # @return [Boolean] Whether the OS is Linux
336
- #
337
- def self.linux?
338
- RbConfig::CONFIG['target_os'] =~ /linux/i
339
- end
340
-
341
- # Test if the OS is Windows.
342
- #
343
- # @return [Boolean] Whether the OS is Windows
344
- #
345
- def self.windows?
346
- RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
347
- end
348
-
349
- end
350
- end
1
+ require 'rbconfig'
2
+ require 'digest/sha1'
3
+
4
+ module Guard
5
+
6
+ autoload :Darwin, 'guard/listeners/darwin'
7
+ autoload :Linux, 'guard/listeners/linux'
8
+ autoload :Windows, 'guard/listeners/windows'
9
+ autoload :Polling, 'guard/listeners/polling'
10
+
11
+ # The Listener is the base class for all listener
12
+ # implementations.
13
+ #
14
+ # @abstract
15
+ #
16
+ class Listener
17
+
18
+ # Default paths that gets ignored by the listener
19
+ DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor]
20
+
21
+ attr_accessor :changed_files
22
+ attr_reader :directory, :ignore_paths
23
+
24
+ def paused?
25
+ @paused
26
+ end
27
+
28
+ # Select the appropriate listener implementation for the
29
+ # current OS and initializes it.
30
+ #
31
+ # @param [Array] args the arguments for the listener
32
+ # @return [Guard::Listener] the chosen listener
33
+ #
34
+ def self.select_and_init(*args)
35
+ if mac? && Darwin.usable?
36
+ Darwin.new(*args)
37
+ elsif linux? && Linux.usable?
38
+ Linux.new(*args)
39
+ elsif windows? && Windows.usable?
40
+ Windows.new(*args)
41
+ else
42
+ UI.info 'Using polling (Please help us to support your system better than that).'
43
+ Polling.new(*args)
44
+ end
45
+ end
46
+
47
+ # Initialize the listener.
48
+ #
49
+ # @param [String] directory the root directory to listen to
50
+ # @option options [Boolean] relativize_paths use only relative paths
51
+ # @option options [Array<String>] ignore_paths the paths to ignore by the listener
52
+ #
53
+ def initialize(directory = Dir.pwd, options = {})
54
+ @directory = directory.to_s
55
+ @sha1_checksums_hash = {}
56
+ @file_timestamp_hash = {}
57
+ @relativize_paths = options.fetch(:relativize_paths, true)
58
+ @changed_files = []
59
+ @paused = false
60
+ @ignore_paths = DEFAULT_IGNORE_PATHS
61
+ @ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
62
+ @watch_all_modifications = options.fetch(:watch_all_modifications, false)
63
+
64
+ update_last_event
65
+ start_reactor
66
+ end
67
+
68
+ # Start the listener thread.
69
+ #
70
+ def start_reactor
71
+ return if ENV["GUARD_ENV"] == 'test'
72
+
73
+ Thread.new do
74
+ loop do
75
+ if @changed_files != [] && !@paused
76
+ changed_files = @changed_files.dup
77
+ clear_changed_files
78
+ ::Guard.run_on_change(changed_files)
79
+ else
80
+ sleep 0.1
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Start watching the root directory.
87
+ #
88
+ def start
89
+ watch(@directory)
90
+ timestamp_files
91
+ end
92
+
93
+ # Stop listening for events.
94
+ #
95
+ def stop
96
+ end
97
+
98
+ # Pause the listener to ignore change events.
99
+ #
100
+ def pause
101
+ @paused = true
102
+ end
103
+
104
+ # Unpause the listener to listen again to change events.
105
+ #
106
+ def run
107
+ @paused = false
108
+ end
109
+
110
+ # Clear the list of changed files.
111
+ #
112
+ def clear_changed_files
113
+ @changed_files.clear
114
+ end
115
+
116
+ # Store a listener callback.
117
+ #
118
+ # @param [Block] callback the callback to store
119
+ #
120
+ def on_change(&callback)
121
+ @callback = callback
122
+ end
123
+
124
+ # Updates the timestamp of the last event.
125
+ #
126
+ def update_last_event
127
+ @last_event = Time.now
128
+ end
129
+
130
+ # Get the modified files.
131
+ #
132
+ # If the `:watch_all_modifications` option is true, then moved and
133
+ # deleted files are also reported, but prefixed by an exclamation point.
134
+ #
135
+ # @example Deleted or moved file
136
+ # !/home/user/dir/file.rb
137
+ #
138
+ # @param [Array<String>] dirs the watched directories
139
+ # @param [Hash] options the listener options
140
+ # @option options [Symbol] all whether to files in sub directories
141
+ # @return [Array<String>] paths of files that have been modified
142
+ #
143
+ def modified_files(dirs, options = {})
144
+ last_event = @last_event
145
+ files = []
146
+ if @watch_all_modifications
147
+ deleted_files = @file_timestamp_hash.collect do |path, ts|
148
+ unless File.exists?(path)
149
+ @sha1_checksums_hash.delete(path)
150
+ @file_timestamp_hash.delete(path)
151
+ "!#{path}"
152
+ end
153
+ end
154
+ files.concat(deleted_files.compact)
155
+ end
156
+ update_last_event
157
+ files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
158
+
159
+ relativize_paths(files)
160
+ end
161
+
162
+ # Register a directory to watch.
163
+ # Must be implemented by the subclasses.
164
+ #
165
+ # @param [String] directory the directory to watch
166
+ #
167
+ def watch(directory)
168
+ raise NotImplementedError, "do whatever you want here, given the directory as only argument"
169
+ end
170
+
171
+ # Get all files that are in the watched directory.
172
+ #
173
+ # @return [Array<String>] the list of files
174
+ #
175
+ def all_files
176
+ potentially_modified_files([@directory], :all => true)
177
+ end
178
+
179
+ # Scopes all given paths to the current directory.
180
+ #
181
+ # @param [Array<String>] paths the paths to change
182
+ # @return [Array<String>] all paths now relative to the current dir
183
+ #
184
+ def relativize_paths(paths)
185
+ return paths unless relativize_paths?
186
+ paths.map do |path|
187
+ path.gsub(%r{^(!)?#{ @directory }/},'\1')
188
+ end
189
+ end
190
+
191
+ # Use paths relative to the current directory.
192
+ #
193
+ # @return [Boolean] whether to use relative or absolute paths
194
+ #
195
+ def relativize_paths?
196
+ !!@relativize_paths
197
+ end
198
+
199
+ # Populate initial timestamp file hash to watch for deleted or moved files.
200
+ #
201
+ def timestamp_files
202
+ all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
203
+ end
204
+
205
+ # Removes the ignored paths from the directory list.
206
+ #
207
+ # @param [Array<String>] dirs the directory to listen to
208
+ # @param [Array<String>] ignore_paths the paths to ignore
209
+ # @return children of the passed dirs that are not in the ignore_paths list
210
+ #
211
+ def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
212
+ Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
213
+ ignore_paths.include?(File.basename(path))
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ # Gets a list of files that are in the modified directories.
220
+ #
221
+ # @param [Array<String>] dirs the list of directories
222
+ # @param [Hash] options the find file option
223
+ # @option options [Symbol] all whether to files in sub directories
224
+ #
225
+ def potentially_modified_files(dirs, options = {})
226
+ paths = exclude_ignored_paths(dirs)
227
+
228
+ if options[:all]
229
+ paths.inject([]) do |array, path|
230
+ if File.file?(path)
231
+ array << path
232
+ else
233
+ array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
234
+ end
235
+ array
236
+ end
237
+ else
238
+ paths.select { |path| File.file?(path) }
239
+ end
240
+ end
241
+
242
+ # Test if the file content has changed.
243
+ #
244
+ # Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
245
+ # both values down to the second for the comparison.
246
+ #
247
+ # ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
248
+ #
249
+ # @param [String] path the file path
250
+ # @param [Time] last_event the time of the last event
251
+ # @return [Boolean] Whether the file content has changed or not.
252
+ #
253
+ def file_modified?(path, last_event)
254
+ ctime = File.ctime(path).to_i
255
+ mtime = File.mtime(path).to_i
256
+ if [mtime, ctime].max == last_event.to_i
257
+ file_content_modified?(path, sha1_checksum(path))
258
+ elsif mtime > last_event.to_i
259
+ set_sha1_checksums_hash(path, sha1_checksum(path))
260
+ true
261
+ elsif @watch_all_modifications
262
+ ts = file_timestamp(path)
263
+ if ts != @file_timestamp_hash[path]
264
+ set_file_timestamp_hash(path, ts)
265
+ true
266
+ end
267
+ else
268
+ false
269
+ end
270
+ rescue
271
+ false
272
+ end
273
+
274
+ # Tests if the file content has been modified by
275
+ # comparing the SHA1 checksum.
276
+ #
277
+ # @param [String] path the file path
278
+ # @param [String] sha1_checksum the checksum of the file
279
+ #
280
+ def file_content_modified?(path, sha1_checksum)
281
+ if @sha1_checksums_hash[path] != sha1_checksum
282
+ set_sha1_checksums_hash(path, sha1_checksum)
283
+ true
284
+ else
285
+ false
286
+ end
287
+ end
288
+
289
+ # Set save a files current timestamp
290
+ #
291
+ # @param [String] path the file path
292
+ # @param [Int] file_timestamp the files modified timestamp
293
+ #
294
+ def set_file_timestamp_hash(path, file_timestamp)
295
+ @file_timestamp_hash[path] = file_timestamp
296
+ end
297
+
298
+ # Set the current checksum of a file.
299
+ #
300
+ # @param [String] path the file path
301
+ # @param [String] sha1_checksum the checksum of the file
302
+ #
303
+ def set_sha1_checksums_hash(path, sha1_checksum)
304
+ @sha1_checksums_hash[path] = sha1_checksum
305
+ end
306
+
307
+ # Gets a files modified timestamp
308
+ #
309
+ # @path [String] path the file path
310
+ # @return [Int] file modified timestamp
311
+ #
312
+ def file_timestamp(path)
313
+ File.mtime(path).to_i
314
+ end
315
+
316
+ # Calculates the SHA1 checksum of a file.
317
+ #
318
+ # @param [String] path the path to the file
319
+ # @return [String] the SHA1 checksum
320
+ #
321
+ def sha1_checksum(path)
322
+ Digest::SHA1.file(path).to_s
323
+ end
324
+
325
+ # Test if the OS is Mac OS X.
326
+ #
327
+ # @return [Boolean] Whether the OS is Mac OS X
328
+ #
329
+ def self.mac?
330
+ RbConfig::CONFIG['target_os'] =~ /darwin/i
331
+ end
332
+
333
+ # Test if the OS is Linux.
334
+ #
335
+ # @return [Boolean] Whether the OS is Linux
336
+ #
337
+ def self.linux?
338
+ RbConfig::CONFIG['target_os'] =~ /linux/i
339
+ end
340
+
341
+ # Test if the OS is Windows.
342
+ #
343
+ # @return [Boolean] Whether the OS is Windows
344
+ #
345
+ def self.windows?
346
+ RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
347
+ end
348
+
349
+ end
350
+ end