listen 1.3.1 → 2.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|