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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -368
- data/README.md +82 -215
- data/lib/listen.rb +2 -36
- data/lib/listen/adapter.rb +23 -304
- data/lib/listen/adapter/base.rb +40 -0
- data/lib/listen/adapter/bsd.rb +93 -0
- data/lib/listen/adapter/darwin.rb +44 -0
- data/lib/listen/adapter/linux.rb +92 -0
- data/lib/listen/adapter/polling.rb +49 -0
- data/lib/listen/adapter/windows.rb +63 -0
- data/lib/listen/change.rb +42 -0
- data/lib/listen/directory.rb +73 -0
- data/lib/listen/file.rb +108 -0
- data/lib/listen/listener.rb +69 -260
- data/lib/listen/record.rb +41 -0
- data/lib/listen/silencer.rb +44 -0
- data/lib/listen/version.rb +1 -1
- metadata +35 -17
- data/lib/listen/adapters/bsd.rb +0 -75
- data/lib/listen/adapters/darwin.rb +0 -48
- data/lib/listen/adapters/linux.rb +0 -81
- data/lib/listen/adapters/polling.rb +0 -58
- data/lib/listen/adapters/windows.rb +0 -91
- data/lib/listen/directory_record.rb +0 -406
- data/lib/listen/turnstile.rb +0 -32
@@ -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
|
data/lib/listen/file.rb
ADDED
@@ -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
|