listen 2.7.5 → 2.7.6
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/.gitignore +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +0 -0
- data/.travis.yml +0 -1
- data/.yardopts +0 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTING.md +0 -0
- data/Gemfile +25 -4
- data/Guardfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +18 -10
- data/Rakefile +0 -0
- data/lib/listen.rb +2 -4
- data/lib/listen/adapter.rb +13 -4
- data/lib/listen/adapter/base.rb +33 -16
- data/lib/listen/adapter/bsd.rb +21 -38
- data/lib/listen/adapter/darwin.rb +17 -25
- data/lib/listen/adapter/linux.rb +34 -52
- data/lib/listen/adapter/polling.rb +9 -25
- data/lib/listen/adapter/tcp.rb +27 -14
- data/lib/listen/adapter/windows.rb +67 -23
- data/lib/listen/change.rb +26 -23
- data/lib/listen/cli.rb +0 -0
- data/lib/listen/directory.rb +47 -58
- data/lib/listen/file.rb +66 -101
- data/lib/listen/listener.rb +214 -155
- data/lib/listen/queue_optimizer.rb +104 -0
- data/lib/listen/record.rb +15 -5
- data/lib/listen/silencer.rb +14 -10
- data/lib/listen/tcp.rb +0 -1
- data/lib/listen/tcp/broadcaster.rb +31 -26
- data/lib/listen/tcp/message.rb +2 -2
- data/lib/listen/version.rb +1 -1
- data/listen.gemspec +1 -1
- data/spec/acceptance/listen_spec.rb +151 -239
- data/spec/acceptance/tcp_spec.rb +125 -134
- data/spec/lib/listen/adapter/base_spec.rb +13 -30
- data/spec/lib/listen/adapter/bsd_spec.rb +7 -35
- data/spec/lib/listen/adapter/darwin_spec.rb +18 -30
- data/spec/lib/listen/adapter/linux_spec.rb +49 -55
- data/spec/lib/listen/adapter/polling_spec.rb +20 -35
- data/spec/lib/listen/adapter/tcp_spec.rb +25 -27
- data/spec/lib/listen/adapter/windows_spec.rb +7 -33
- data/spec/lib/listen/adapter_spec.rb +10 -10
- data/spec/lib/listen/change_spec.rb +55 -57
- data/spec/lib/listen/directory_spec.rb +105 -155
- data/spec/lib/listen/file_spec.rb +186 -73
- data/spec/lib/listen/listener_spec.rb +233 -216
- data/spec/lib/listen/record_spec.rb +60 -22
- data/spec/lib/listen/silencer_spec.rb +48 -75
- data/spec/lib/listen/tcp/broadcaster_spec.rb +78 -69
- data/spec/lib/listen/tcp/listener_spec.rb +28 -71
- data/spec/lib/listen/tcp/message_spec.rb +48 -14
- data/spec/lib/listen_spec.rb +3 -3
- data/spec/spec_helper.rb +6 -3
- data/spec/support/acceptance_helper.rb +250 -31
- data/spec/support/fixtures_helper.rb +6 -4
- data/spec/support/platform_helper.rb +2 -2
- metadata +5 -5
- data/lib/listen/tcp/listener.rb +0 -108
@@ -6,15 +6,9 @@ module Listen
|
|
6
6
|
# file IO than the other implementations.
|
7
7
|
#
|
8
8
|
class Polling < Base
|
9
|
-
|
10
|
-
|
11
|
-
def self.usable?
|
12
|
-
true
|
13
|
-
end
|
9
|
+
OS_REGEXP = // # match any
|
14
10
|
|
15
|
-
|
16
|
-
Thread.new { _poll_directories }
|
17
|
-
end
|
11
|
+
DEFAULT_POLLING_LATENCY = 1.0
|
18
12
|
|
19
13
|
private
|
20
14
|
|
@@ -22,26 +16,16 @@ module Listen
|
|
22
16
|
listener.options[:latency] || DEFAULT_POLLING_LATENCY
|
23
17
|
end
|
24
18
|
|
25
|
-
def
|
26
|
-
_napped_loop do
|
27
|
-
listener.directories.each do |path|
|
28
|
-
_notify_change(path, type: 'Dir', recursive: true)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def _napped_loop
|
19
|
+
def _run
|
34
20
|
loop do
|
35
|
-
|
21
|
+
start = Time.now.to_f
|
22
|
+
_directories.each do |path|
|
23
|
+
_notify_change(:dir, path, recursive: true)
|
24
|
+
nap_time = _latency - (Time.now.to_f - start)
|
25
|
+
sleep(nap_time) if nap_time > 0
|
26
|
+
end
|
36
27
|
end
|
37
28
|
end
|
38
|
-
|
39
|
-
def _nap_time
|
40
|
-
start = Time.now.to_f
|
41
|
-
yield
|
42
|
-
nap_time = _latency - (Time.now.to_f - start)
|
43
|
-
sleep(nap_time) if nap_time > 0
|
44
|
-
end
|
45
29
|
end
|
46
30
|
end
|
47
31
|
end
|
data/lib/listen/adapter/tcp.rb
CHANGED
@@ -1,33 +1,46 @@
|
|
1
1
|
require 'celluloid/io'
|
2
2
|
|
3
|
+
require 'listen/tcp/message'
|
4
|
+
|
3
5
|
module Listen
|
4
6
|
module Adapter
|
5
7
|
# Adapter to receive file system modifications over TCP
|
6
8
|
class TCP < Base
|
9
|
+
OS_REGEXP = // # match any
|
10
|
+
|
7
11
|
include Celluloid::IO
|
8
12
|
|
9
13
|
finalizer :finalize
|
10
14
|
|
11
15
|
attr_reader :buffer, :socket
|
12
16
|
|
13
|
-
def self.usable?
|
14
|
-
true
|
15
|
-
end
|
16
|
-
|
17
17
|
# Initializes and starts a Celluloid::IO-powered TCP-recipient
|
18
18
|
def start
|
19
|
+
attempts = 3
|
19
20
|
@socket = TCPSocket.new(listener.host, listener.port)
|
20
21
|
@buffer = ''
|
21
|
-
run
|
22
|
+
async.run
|
23
|
+
rescue Celluloid::Task::TerminatedError
|
24
|
+
_log :debug, "TCP adapter was terminated: #{$!.inspect}"
|
25
|
+
rescue Errno::ECONNREFUSED
|
26
|
+
sleep 1
|
27
|
+
attempts -= 1
|
28
|
+
_log :warn, "TCP.start: #{$!.inspect}"
|
29
|
+
retry if retries > 0
|
30
|
+
_log :error, "TCP.start: #{$!.inspect}:#{$@.join("\n")}"
|
31
|
+
raise
|
32
|
+
rescue
|
33
|
+
_log :error, "TCP.start: #{$!.inspect}:#{$@.join("\n")}"
|
34
|
+
raise
|
22
35
|
end
|
23
36
|
|
24
37
|
# Cleans up buffer and socket
|
25
38
|
def finalize
|
26
39
|
@buffer = nil
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
40
|
+
return unless @socket
|
41
|
+
|
42
|
+
@socket.close
|
43
|
+
@socket = nil
|
31
44
|
end
|
32
45
|
|
33
46
|
# Number of bytes to receive at a time
|
@@ -46,15 +59,15 @@ module Listen
|
|
46
59
|
while (message = Listen::TCP::Message.from_buffer(@buffer))
|
47
60
|
handle_message(message)
|
48
61
|
end
|
62
|
+
rescue
|
63
|
+
_log :error, "TCP.handle_data crashed: #{$!}:#{$@.join("\n")}"
|
64
|
+
raise
|
49
65
|
end
|
50
66
|
|
51
67
|
# Handles incoming message by notifying of path changes
|
52
68
|
def handle_message(message)
|
53
|
-
|
54
|
-
|
55
|
-
_notify_change(path, change: change.to_sym)
|
56
|
-
end
|
57
|
-
end
|
69
|
+
type, modification, path, _ = message.object
|
70
|
+
_notify_change(type.to_sym, path, change: modification.to_sym)
|
58
71
|
end
|
59
72
|
|
60
73
|
def self.local_fs?
|
@@ -3,8 +3,8 @@ module Listen
|
|
3
3
|
# Adapter implementation for Windows `wdm`.
|
4
4
|
#
|
5
5
|
class Windows < Base
|
6
|
-
|
7
|
-
|
6
|
+
OS_REGEXP = /mswin|mingw|cygwin/i
|
7
|
+
|
8
8
|
BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
|
9
9
|
Please add the following to your Gemfile to avoid polling for changes:
|
10
10
|
require 'rbconfig'
|
@@ -14,39 +14,83 @@ module Listen
|
|
14
14
|
EOS
|
15
15
|
|
16
16
|
def self.usable?
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
17
|
+
return false unless super
|
18
|
+
require 'wdm'
|
19
|
+
true
|
21
20
|
rescue LoadError
|
21
|
+
_log :debug, "wdm - load failed: #{$!}:#{$@.join("\n")}"
|
22
22
|
Kernel.warn BUNDLER_DECLARE_GEM
|
23
23
|
false
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
private
|
27
|
+
|
28
|
+
def _configure
|
29
|
+
_log :debug, 'wdm - starting...'
|
30
|
+
@worker = WDM::Monitor.new
|
31
|
+
_directories.each do |path|
|
32
|
+
@worker.watch_recursively(path.to_s, :files, &_file_callback)
|
33
|
+
@worker.watch_recursively(path.to_s, :directories, &_dir_callback)
|
34
|
+
@worker.watch_recursively(path.to_s, :attributes, :last_write,
|
35
|
+
&_attr_callback)
|
36
|
+
end
|
29
37
|
end
|
30
38
|
|
31
|
-
|
39
|
+
def _run
|
40
|
+
@worker.run!
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
43
|
+
def _file_callback
|
44
|
+
lambda do |change|
|
45
|
+
begin
|
46
|
+
path = _path(change.path)
|
47
|
+
_log :debug, "wdm - FILE callback: #{change.inspect}"
|
48
|
+
options = { change: _change(change.type) }
|
49
|
+
_notify_change(:file, path, options)
|
50
|
+
rescue
|
51
|
+
_log :error, "wdm - callback failed: #{$!}:#{$@.join("\n")}"
|
52
|
+
raise
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
45
56
|
|
46
|
-
def
|
57
|
+
def _attr_callback
|
47
58
|
lambda do |change|
|
48
|
-
|
49
|
-
|
59
|
+
begin
|
60
|
+
path = _path(change.path)
|
61
|
+
return if path.directory?
|
62
|
+
|
63
|
+
_log :debug, "wdm - ATTR callback: #{change.inspect}"
|
64
|
+
options = { change: _change(change.type) }
|
65
|
+
_notify_change(:file, _path(change.path), options)
|
66
|
+
rescue
|
67
|
+
_log :error, "wdm - callback failed: #{$!}:#{$@.join("\n")}"
|
68
|
+
raise
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def _dir_callback
|
74
|
+
lambda do |change|
|
75
|
+
begin
|
76
|
+
path = _path(change.path)
|
77
|
+
_log :debug, "wdm - DIR callback: #{change.inspect}"
|
78
|
+
if change.type == :removed
|
79
|
+
_notify_change(:dir, path.dirname)
|
80
|
+
elsif change.type == :added
|
81
|
+
_notify_change(:dir, path)
|
82
|
+
else
|
83
|
+
# do nothing - changed directory means either:
|
84
|
+
# - removed subdirs (handled above)
|
85
|
+
# - added subdirs (handled above)
|
86
|
+
# - removed files (handled by _file_callback)
|
87
|
+
# - added files (handled by _file_callback)
|
88
|
+
# so what's left?
|
89
|
+
end
|
90
|
+
rescue
|
91
|
+
_log :error, "wdm - callback failed: #{$!}:#{$@.join("\n")}"
|
92
|
+
raise
|
93
|
+
end
|
50
94
|
end
|
51
95
|
end
|
52
96
|
|
@@ -55,7 +99,7 @@ module Listen
|
|
55
99
|
end
|
56
100
|
|
57
101
|
def _change(type)
|
58
|
-
{ modified: [:modified],
|
102
|
+
{ modified: [:modified, :attrib], # TODO: is attrib really passed?
|
59
103
|
added: [:added, :renamed_new_file],
|
60
104
|
removed: [:removed, :renamed_old_file] }.each do |change, types|
|
61
105
|
return change if types.include?(type)
|
data/lib/listen/change.rb
CHANGED
@@ -11,41 +11,44 @@ module Listen
|
|
11
11
|
@listener = listener
|
12
12
|
end
|
13
13
|
|
14
|
-
def change(path, options)
|
14
|
+
def change(type, path, options = {})
|
15
15
|
change = options[:change]
|
16
16
|
cookie = options[:cookie]
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
return
|
18
|
+
if !cookie && listener.silencer.silenced?(path, type)
|
19
|
+
_log :debug, "(silenced): #{path.inspect}"
|
20
|
+
return
|
21
21
|
end
|
22
22
|
|
23
|
+
log_details = options[:silence] && 'recording' || change || 'unknown'
|
24
|
+
_log :debug, "#{log_details}: #{type}:#{path} (#{options.inspect})"
|
25
|
+
|
23
26
|
if change
|
24
|
-
|
27
|
+
listener.queue(type, change, path, cookie ? { cookie: cookie } : {})
|
25
28
|
else
|
26
|
-
|
29
|
+
return unless (record = listener.sync(:record))
|
30
|
+
record.async.still_building! if options[:build]
|
31
|
+
|
32
|
+
if type == :dir
|
33
|
+
return unless (change_queue = listener.async(:change_pool))
|
34
|
+
Directory.scan(change_queue, record, path, options)
|
35
|
+
else
|
36
|
+
change = File.change(record, path)
|
37
|
+
return if !change || options[:silence]
|
38
|
+
listener.queue(:file, change, path)
|
39
|
+
end
|
27
40
|
end
|
41
|
+
rescue Celluloid::Task::TerminatedError
|
42
|
+
_log :debug, "Change#change was terminated: #{$!.inspect}"
|
43
|
+
rescue RuntimeError
|
44
|
+
_log :error, "Change#change crashed #{$!.inspect}:#{$@.join("\n")}"
|
45
|
+
raise
|
28
46
|
end
|
29
47
|
|
30
48
|
private
|
31
49
|
|
32
|
-
def
|
33
|
-
|
34
|
-
if change && listener.listen? && !options[:silence]
|
35
|
-
_notify_listener(change, path)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def _dir_change(path, options)
|
40
|
-
Directory.new(listener, path, options).scan
|
41
|
-
end
|
42
|
-
|
43
|
-
def _notify_listener(change, path, options = {})
|
44
|
-
listener.changes << { change => path }.merge(options)
|
45
|
-
end
|
46
|
-
|
47
|
-
def _silencer
|
48
|
-
listener.registry[:silencer]
|
50
|
+
def _log(type, message)
|
51
|
+
Celluloid.logger.send(type, message)
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
data/lib/listen/cli.rb
CHANGED
File without changes
|
data/lib/listen/directory.rb
CHANGED
@@ -1,78 +1,67 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Listen
|
2
4
|
class Directory
|
3
|
-
|
4
|
-
|
5
|
-
def initialize(listener, path, options = {})
|
6
|
-
@listener = listener
|
7
|
-
@path = path
|
8
|
-
@options = options
|
9
|
-
end
|
5
|
+
def self.scan(queue, sync_record, path, options = {})
|
6
|
+
return unless (record = sync_record.async)
|
10
7
|
|
11
|
-
|
12
|
-
_update_record
|
13
|
-
_all_entries.each do |entry_path, data|
|
14
|
-
case data[:type]
|
15
|
-
when 'File'
|
16
|
-
_async_change(entry_path, options.merge(type: 'File'))
|
17
|
-
when 'Dir'
|
18
|
-
if _recursive_scan?(entry_path)
|
19
|
-
_async_change(entry_path, options.merge(type: 'Dir'))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
8
|
+
previous = sync_record.dir_entries(path)
|
24
9
|
|
25
|
-
|
10
|
+
record.set_path(:dir, path)
|
11
|
+
current = Set.new(path.children)
|
26
12
|
|
27
|
-
|
28
|
-
|
29
|
-
|
13
|
+
if options[:silence]
|
14
|
+
_log :debug, "Recording: #{path}: #{options.inspect}"\
|
15
|
+
" [#{previous.inspect}] -> (#{current.inspect})"
|
30
16
|
else
|
31
|
-
|
17
|
+
_log :debug, "Scanning: #{path}: #{options.inspect}"\
|
18
|
+
" [#{previous.inspect}] -> (#{current.inspect})"
|
32
19
|
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def _all_entries
|
36
|
-
_record_entries.merge(_entries)
|
37
|
-
end
|
38
|
-
|
39
|
-
def _entries
|
40
|
-
return {} unless ::Dir.exist?(path)
|
41
20
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
entry_path = path.join(entry_path)
|
49
|
-
if entry_path.file?
|
50
|
-
'File'
|
51
|
-
elsif entry_path.directory?
|
52
|
-
'Dir'
|
21
|
+
current.each do |full_path|
|
22
|
+
if full_path.directory?
|
23
|
+
_change(queue, :dir, full_path, options)
|
24
|
+
else
|
25
|
+
_change(queue, :file, full_path, options)
|
26
|
+
end
|
53
27
|
end
|
54
|
-
end
|
55
28
|
|
56
|
-
|
57
|
-
|
58
|
-
future.value
|
59
|
-
end
|
29
|
+
previous.reject! { |entry, _| current.include? path + entry }
|
30
|
+
_async_changes(path, queue, previous, options)
|
60
31
|
|
61
|
-
|
62
|
-
|
32
|
+
rescue Errno::ENOENT
|
33
|
+
record.unset_path(path)
|
34
|
+
_async_changes(path, queue, previous, options)
|
35
|
+
|
36
|
+
rescue Errno::ENOTDIR
|
37
|
+
# TODO: path not tested
|
38
|
+
record.unset_path(path)
|
39
|
+
_async_changes(path, queue, previous, options)
|
40
|
+
_change(queue, :file, path, options)
|
41
|
+
rescue
|
42
|
+
_log :warn, "scanning DIED: #{$!}:#{$@.join("\n")}"
|
43
|
+
raise
|
63
44
|
end
|
64
45
|
|
65
|
-
def
|
66
|
-
|
46
|
+
def self._async_changes(path, queue, previous, options)
|
47
|
+
previous.each do |entry, data|
|
48
|
+
_change(queue, data[:type], path + entry, options)
|
49
|
+
end
|
67
50
|
end
|
68
51
|
|
69
|
-
def
|
70
|
-
|
52
|
+
def self._change(queue, type, full_path, options)
|
53
|
+
return queue.change(type, full_path, options) if type == :dir
|
54
|
+
opts = options.dup
|
55
|
+
opts.delete(:recursive)
|
56
|
+
if opts.empty?
|
57
|
+
queue.change(type, full_path)
|
58
|
+
else
|
59
|
+
queue.change(type, full_path, opts)
|
60
|
+
end
|
71
61
|
end
|
72
62
|
|
73
|
-
def
|
74
|
-
|
75
|
-
_change_pool.async.change(entry_path, options)
|
63
|
+
def self._log(type, message)
|
64
|
+
Celluloid.logger.send(type, message)
|
76
65
|
end
|
77
66
|
end
|
78
67
|
end
|
data/lib/listen/file.rb
CHANGED
@@ -1,118 +1,83 @@
|
|
1
1
|
module Listen
|
2
2
|
class File
|
3
|
-
|
3
|
+
def self.change(record, path)
|
4
|
+
lstat = path.lstat
|
4
5
|
|
5
|
-
|
6
|
-
@listener = listener
|
7
|
-
@path = path
|
8
|
-
@data = { type: 'File' }
|
9
|
-
end
|
10
|
-
|
11
|
-
def change
|
12
|
-
if _existing_path? && _modified?
|
13
|
-
_set_record_data
|
14
|
-
:modified
|
15
|
-
elsif _new_path?
|
16
|
-
_set_record_data
|
17
|
-
:added
|
18
|
-
elsif _removed_path?
|
19
|
-
_unset_record_data
|
20
|
-
:removed
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def _new_path?
|
27
|
-
_exist? && !_record_data?
|
28
|
-
end
|
29
|
-
|
30
|
-
def _existing_path?
|
31
|
-
_exist? && _record_data?
|
32
|
-
end
|
33
|
-
|
34
|
-
def _removed_path?
|
35
|
-
!_exist?
|
36
|
-
end
|
37
|
-
|
38
|
-
def _record_data?
|
39
|
-
!_record_data.empty?
|
40
|
-
end
|
41
|
-
|
42
|
-
def _exist?
|
43
|
-
@exist ||= ::File.exist?(path)
|
44
|
-
end
|
45
|
-
|
46
|
-
def _modified?
|
47
|
-
_mtime > _record_data[:mtime] || _mode_modified? || _content_modified?
|
48
|
-
end
|
49
|
-
|
50
|
-
def _mode_modified?
|
51
|
-
_mode != _record_data[:mode]
|
52
|
-
end
|
6
|
+
data = { mtime: lstat.mtime.to_f, mode: lstat.mode }
|
53
7
|
|
54
|
-
|
55
|
-
# Only check if in the same seconds (mtime == current time).
|
56
|
-
# MD5 is eager loaded, so the first time it'll always return false.
|
57
|
-
#
|
58
|
-
def _content_modified?
|
59
|
-
return false unless RbConfig::CONFIG['target_os'] =~ /darwin/i
|
60
|
-
return false unless _mtime.to_i == Time.now.to_i
|
8
|
+
record_data = record.file_data(path)
|
61
9
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
else
|
66
|
-
_set_record_data
|
67
|
-
false
|
10
|
+
if record_data.empty?
|
11
|
+
record.async.set_path(:file, path, data)
|
12
|
+
return :added
|
68
13
|
end
|
69
|
-
end
|
70
14
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def _unset_record_data
|
77
|
-
_record.async.unset_path(path)
|
78
|
-
end
|
79
|
-
|
80
|
-
def _new_data
|
81
|
-
data = { mtime: _mtime, mode: _mode }
|
82
|
-
data[:md5] = md5 if md5
|
83
|
-
data
|
84
|
-
end
|
85
|
-
|
86
|
-
def _record_data
|
87
|
-
@_record_data ||= _record.future.file_data(path).value
|
88
|
-
end
|
15
|
+
if data[:mode] != record_data[:mode]
|
16
|
+
record.async.set_path(:file, path, data)
|
17
|
+
return :modified
|
18
|
+
end
|
89
19
|
|
90
|
-
|
91
|
-
|
92
|
-
|
20
|
+
if data[:mtime] != record_data[:mtime]
|
21
|
+
record.async.set_path(:file, path, data)
|
22
|
+
return :modified
|
23
|
+
end
|
93
24
|
|
94
|
-
|
95
|
-
|
25
|
+
unless /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING']
|
26
|
+
if self.inaccurate_mac_time?(lstat)
|
27
|
+
# Check if change happened within 1 second (maybe it's even
|
28
|
+
# too much, e.g. 0.3-0.5 could be sufficient).
|
29
|
+
#
|
30
|
+
# With rb-fsevent, there's a (configurable) latency between
|
31
|
+
# when file was changed and when the event was triggered.
|
32
|
+
#
|
33
|
+
# If a file is saved at ???14.998, by the time the event is
|
34
|
+
# actually received by Listen, the time could already be e.g.
|
35
|
+
# ???15.7.
|
36
|
+
#
|
37
|
+
# And since Darwin adapter uses directory scanning, the file
|
38
|
+
# mtime may be the same (e.g. file was changed at ???14.001,
|
39
|
+
# then at ???14.998, but the fstat time would be ???14.0 in
|
40
|
+
# both cases).
|
41
|
+
#
|
42
|
+
# If change happend at ???14.999997, the mtime is 14.0, so for
|
43
|
+
# an mtime=???14.0 we assume it could even be almost ???15.0
|
44
|
+
#
|
45
|
+
# So if Time.now.to_f is ???15.999998 and stat reports mtime
|
46
|
+
# at ???14.0, then event was due to that file'd change when:
|
47
|
+
#
|
48
|
+
# ???15.999997 - ???14.999998 < 1.0s
|
49
|
+
#
|
50
|
+
# So the "2" is "1 + 1" (1s to cover rb-fsevent latency +
|
51
|
+
# 1s maximum difference between real mtime and that recorded
|
52
|
+
# in the file system)
|
53
|
+
#
|
54
|
+
if data[:mtime].to_i + 2 > Time.now.to_f
|
55
|
+
begin
|
56
|
+
md5 = Digest::MD5.file(path).digest
|
57
|
+
record.async.set_path(:file, path, data.merge(md5: md5))
|
58
|
+
:modified if record_data[:md5] && md5 != record_data[:md5]
|
59
|
+
|
60
|
+
rescue SystemCallError
|
61
|
+
# ignore failed md5
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue SystemCallError
|
67
|
+
record.async.unset_path(path)
|
68
|
+
:removed
|
96
69
|
rescue
|
97
|
-
|
70
|
+
Celluloid::Logger.debug "lstat failed for: #{path} (#{$!})"
|
71
|
+
raise
|
98
72
|
end
|
99
73
|
|
100
|
-
def
|
101
|
-
|
102
|
-
rescue
|
103
|
-
nil
|
104
|
-
end
|
74
|
+
def self.inaccurate_mac_time?(stat)
|
75
|
+
# 'mac' means Modified/Accessed/Created
|
105
76
|
|
106
|
-
|
107
|
-
|
108
|
-
rescue
|
109
|
-
nil
|
110
|
-
end
|
77
|
+
# Since precision depends on mounted FS (e.g. you can have a FAT partiion
|
78
|
+
# mounted on Linux), check for fields with a remainder to detect this
|
111
79
|
|
112
|
-
|
113
|
-
@md5 = Digest::MD5.file(path).digest
|
114
|
-
rescue
|
115
|
-
nil
|
80
|
+
[stat.mtime, stat.ctime, stat.atime].map(&:usec).all?(&:zero?)
|
116
81
|
end
|
117
82
|
end
|
118
83
|
end
|