listen 1.3.1 → 2.0.0.beta.1

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.
@@ -0,0 +1,44 @@
1
+ module Listen
2
+ module Adapter
3
+
4
+ # Adapter implementation for Mac OS X `FSEvents`.
5
+ #
6
+ class Darwin < Base
7
+
8
+ def self.usable?
9
+ RbConfig::CONFIG['target_os'] =~ /darwin(1.+)?$/i
10
+ end
11
+
12
+ def initialize(listener)
13
+ require 'rb-fsevent'
14
+ super
15
+ end
16
+
17
+ def start
18
+ worker = _init_worker
19
+ worker.run
20
+ end
21
+
22
+ private
23
+
24
+ # Initializes a FSEvent worker and adds a watcher for
25
+ # each directory listened.
26
+ #
27
+ def _init_worker
28
+ FSEvent.new.tap do |worker|
29
+ worker.watch(_directories_path, latency: _latency) do |changes|
30
+ _changes_path(changes).each { |path| _notify_change(path, type: 'Dir') }
31
+ end
32
+ end
33
+ end
34
+
35
+ def _changes_path(changes)
36
+ changes.map do |path|
37
+ path.sub!(/\/$/, '')
38
+ Pathname.new(path)
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,92 @@
1
+ module Listen
2
+ module Adapter
3
+
4
+ # Listener implementation for Linux `inotify`.
5
+ #
6
+ class Linux < Base
7
+ # Watched inotify events
8
+ #
9
+ # @see http://www.tin.org/bin/man.cgi?section=7&topic=inotify
10
+ # @see https://github.com/nex3/rb-inotify/blob/master/lib/rb-inotify/notifier.rb#L99-L177
11
+ #
12
+ EVENTS = [:recursive, :attrib, :create, :delete, :move, :close_write]
13
+
14
+ # The message to show when the limit of inotify watchers is not enough
15
+ #
16
+ INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
17
+ Listen error: unable to monitor directories for changes.
18
+
19
+ Please head to https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers
20
+ for information on how to solve this issue.
21
+ EOS
22
+
23
+ def self.usable?
24
+ RbConfig::CONFIG['target_os'] =~ /linux/i
25
+ end
26
+
27
+ def initialize(listener)
28
+ require 'rb-inotify'
29
+ super
30
+ end
31
+
32
+ def start
33
+ worker = _init_worker
34
+ worker.run
35
+ rescue Errno::ENOSPC
36
+ abort(INOTIFY_LIMIT_MESSAGE)
37
+ end
38
+
39
+ private
40
+
41
+ # Initializes a INotify worker and adds a watcher for
42
+ # each directory passed to the adapter.
43
+ #
44
+ # @return [INotify::Notifier] initialized worker
45
+ def _init_worker
46
+ INotify::Notifier.new.tap do |worker|
47
+ _directories_path.each { |path| worker.watch(path, *EVENTS, &_worker_callback) }
48
+ end
49
+ end
50
+
51
+ def _worker_callback
52
+ lambda do |event|
53
+ next if _skip_event?(event)
54
+
55
+ if _dir_event?(event)
56
+ _notify_change(_event_path(event), type: 'Dir')
57
+ else
58
+ _notify_change(_event_path(event), type: 'file', change: _change(event.flags))
59
+ end
60
+ end
61
+ end
62
+
63
+ def _skip_event?(event)
64
+ # Event on root directory
65
+ return true if event.name == ""
66
+ # INotify reports changes to files inside directories as events
67
+ # on the directories themselves too.
68
+ #
69
+ # @see http://linux.die.net/man/7/inotify
70
+ return true if _dir_event?(event) && (event.flags & [:close, :modify]).any?
71
+ end
72
+
73
+ def _change(event_flags)
74
+ { modified: [:attrib],
75
+ added: [:moved_to, :create],
76
+ removed: [:moved_from, :delete] }.each do |change, flags|
77
+ return change unless (flags & event_flags).empty?
78
+ end
79
+ nil
80
+ end
81
+
82
+ def _dir_event?(event)
83
+ event.flags.include?(:isdir)
84
+ end
85
+
86
+ def _event_path(event)
87
+ Pathname.new(event.absolute_name)
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,49 @@
1
+ module Listen
2
+ module Adapter
3
+
4
+ # Polling Adapter that works cross-platform and
5
+ # has no dependencies. This is the adapter that
6
+ # uses the most CPU processing power and has higher
7
+ # file IO than the other implementations.
8
+ #
9
+ class Polling < Base
10
+ DEFAULT_POLLING_LATENCY = 1.0
11
+
12
+ def self.usable?
13
+ true
14
+ end
15
+
16
+ def start
17
+ _poll_directories
18
+ end
19
+
20
+ private
21
+
22
+ def _latency
23
+ listener.options[:latency] || DEFAULT_POLLING_LATENCY
24
+ end
25
+
26
+ def _poll_directories
27
+ _napped_loop do
28
+ listener.directories.each do |path|
29
+ _notify_change(path, type: 'Dir', recursive: true)
30
+ end
31
+ end
32
+ end
33
+
34
+ def _napped_loop
35
+ loop do
36
+ _nap_time { yield }
37
+ end
38
+ end
39
+
40
+ def _nap_time
41
+ start = Time.now.to_f
42
+ yield
43
+ nap_time = _latency - (Time.now.to_f - start)
44
+ sleep(nap_time) if nap_time > 0
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,63 @@
1
+ module Listen
2
+ module Adapter
3
+
4
+ # Adapter implementation for Windows `wdm`.
5
+ #
6
+ class Windows < Base
7
+
8
+ # The message to show when wdm gem isn't available
9
+ #
10
+ BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
11
+ Please add the following to your Gemfile to avoid polling for changes:
12
+ require 'rbconfig'
13
+ gem 'wdm', '>= 0.1.0' if RbConfig::CONFIG['target_os'] =~ /mswin|mingw|cygwin/i
14
+ EOS
15
+
16
+ def self.usable?
17
+ if RbConfig::CONFIG['target_os'] =~ /mswin|mingw|cygwin/i
18
+ require 'wdm'
19
+ true
20
+ end
21
+ rescue Gem::LoadError
22
+ Kernel.warn BUNDLER_DECLARE_GEM
23
+ end
24
+
25
+ def start
26
+ worker = _init_worker
27
+ worker.run!
28
+ end
29
+
30
+ private
31
+
32
+ # Initializes a WDM monitor and adds a watcher for
33
+ # each directory passed to the adapter.
34
+ #
35
+ # @return [WDM::Monitor] initialized worker
36
+ def _init_worker
37
+ WDM::Monitor.new.tap do |worker|
38
+ _directories_path.each { |path| worker.watch_recursively(path, &_worker_callback) }
39
+ end
40
+ end
41
+
42
+ def _worker_callback
43
+ lambda do |change|
44
+ _notify_change(_path(change.path), type: 'file', change: _change(change.type))
45
+ end
46
+ end
47
+
48
+ def _path(path)
49
+ Pathname.new(path)
50
+ end
51
+
52
+ def _change(type)
53
+ { modified: [:modified],
54
+ added: [:added, :renamed_new_file],
55
+ removed: [:removed, :renamed_old_file] }.each do |change, types|
56
+ return change if types.include?(type)
57
+ end
58
+ nil
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ require 'listen/file'
2
+ require 'listen/directory'
3
+ require 'listen/silencer'
4
+
5
+ module Listen
6
+ class Change
7
+ include Celluloid
8
+
9
+ attr_accessor :listener, :silencer
10
+
11
+ def initialize(listener)
12
+ @listener = listener
13
+ @silencer = Silencer.new(listener.options)
14
+ end
15
+
16
+ def change(path, options)
17
+ return if silencer.silenced?(path)
18
+ if change = options[:change]
19
+ _notify_listener(change, path)
20
+ else
21
+ send("_#{options[:type].downcase}_change", path, options)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def _file_change(path, options)
28
+ change = File.new(path).change
29
+ if change && listener.listen? && !options[:silence]
30
+ _notify_listener(change, path)
31
+ end
32
+ end
33
+
34
+ def _dir_change(path, options)
35
+ Directory.new(path, options).scan
36
+ end
37
+
38
+ def _notify_listener(change, path)
39
+ listener.changes << { change => path }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ module Listen
2
+ class Directory
3
+ attr_accessor :path, :options
4
+
5
+ def initialize(path, options = {})
6
+ @path = path
7
+ @options = options
8
+ end
9
+
10
+ def scan
11
+ _update_record
12
+ _all_entries.each do |entry_path, data|
13
+ case data[:type]
14
+ when 'File' then _async_change(entry_path, options.merge(type: 'File'))
15
+ when 'Dir'
16
+ _async_change(entry_path, options.merge(type: 'Dir')) if _recursive_scan?(entry_path)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def _update_record
24
+ if ::Dir.exists?(path)
25
+ _record.async.set_path(path, { type: 'Dir'})
26
+ else
27
+ _record.async.unset_path(path)
28
+ end
29
+ end
30
+
31
+ def _all_entries
32
+ _record_entries.merge(_entries)
33
+ end
34
+
35
+ def _entries
36
+ return {} unless ::Dir.exists?(path)
37
+ entries = ::Dir.entries(path) - %w[. ..]
38
+ entries = entries.map { |entry| [entry, type: _entry_type(entry)] }
39
+ Hash[*entries.flatten]
40
+ end
41
+
42
+ def _entry_type(entry_path)
43
+ entry_path = path.join(entry_path)
44
+ if entry_path.file?
45
+ 'File'
46
+ elsif entry_path.directory?
47
+ 'Dir'
48
+ end
49
+ end
50
+
51
+ def _record_entries
52
+ future = _record.future.dir_entries(path)
53
+ future.value
54
+ end
55
+
56
+ def _record
57
+ Celluloid::Actor[:listen_record]
58
+ end
59
+
60
+ def _change_pool
61
+ Celluloid::Actor[:listen_change_pool]
62
+ end
63
+
64
+ def _recursive_scan?(path)
65
+ !::Dir.exists?(path) || options[:recursive]
66
+ end
67
+
68
+ def _async_change(entry_path, options)
69
+ entry_path = path.join(entry_path)
70
+ _change_pool.async.change(entry_path, options)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,108 @@
1
+ module Listen
2
+ class File
3
+ attr_accessor :path, :data
4
+
5
+ def initialize(path)
6
+ @path = path
7
+ @data = { type: 'File' }
8
+ end
9
+
10
+ def change
11
+ if _existing_path? && _modified?
12
+ _set_record_data
13
+ :modified
14
+ elsif _new_path?
15
+ _set_record_data
16
+ :added
17
+ elsif _removed_path?
18
+ _unset_record_data
19
+ :removed
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def _new_path?
26
+ _exist? && !_record_data?
27
+ end
28
+
29
+ def _existing_path?
30
+ _exist? && _record_data?
31
+ end
32
+
33
+ def _removed_path?
34
+ !_exist?
35
+ end
36
+
37
+ def _record_data?
38
+ !_record_data.empty?
39
+ end
40
+
41
+ def _exist?
42
+ @exist ||= ::File.exist?(path)
43
+ end
44
+
45
+ def _modified?
46
+ _mtime > _record_data[:mtime] || _mode_modified? || _content_modified?
47
+ end
48
+
49
+ def _mode_modified?
50
+ _mode != _record_data[:mode]
51
+ end
52
+
53
+ # Only useful on Darwin because of the file mtime second precision
54
+ #
55
+ def _content_modified?
56
+ _record_data[:md5] && _md5 != _record_data[:md5]
57
+ end
58
+
59
+ def _set_record_data
60
+ @data.merge!(_new_data)
61
+ _record.async.set_path(path, data)
62
+ end
63
+
64
+ def _unset_record_data
65
+ _record.async.unset_path(path)
66
+ end
67
+
68
+ # Only Darwin need md5 comparaison because of the file mtime second precision
69
+ #
70
+ def _new_data
71
+ data = { mtime: _mtime, mode: _mode }
72
+ data[:md5] = _md5 if RbConfig::CONFIG['target_os'] =~ /darwin/i
73
+ data
74
+ end
75
+
76
+ def _record_data
77
+ @_record_data ||= _record.future.file_data(path).value
78
+ end
79
+
80
+ def _record
81
+ Celluloid::Actor[:listen_record]
82
+ end
83
+
84
+ def _mtime
85
+ @mtime ||= _lstat.mtime.to_f
86
+ rescue
87
+ 0.0
88
+ end
89
+
90
+ def _mode
91
+ @mode ||= _lstat.mode
92
+ rescue
93
+ nil
94
+ end
95
+
96
+ def _lstat
97
+ @lstat ||= ::File.lstat(path)
98
+ rescue
99
+ nil
100
+ end
101
+
102
+ def _md5
103
+ @md5 ||= Digest::MD5.file(path).digest
104
+ rescue
105
+ nil
106
+ end
107
+ end
108
+ end