filewatcher 0.5.4 → 2.0.0.beta1
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 +5 -5
- data/lib/filewatcher.rb +79 -138
- data/lib/filewatcher/cycles.rb +56 -0
- data/lib/filewatcher/snapshot.rb +65 -0
- data/lib/filewatcher/snapshots.rb +56 -0
- data/lib/filewatcher/spec_helper.rb +66 -0
- data/lib/filewatcher/spec_helper/watch_run.rb +74 -0
- data/lib/filewatcher/version.rb +5 -0
- data/spec/filewatcher/snapshot_spec.rb +67 -0
- data/spec/filewatcher/version_spec.rb +11 -0
- data/spec/filewatcher_spec.rb +289 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/spec_helper/ruby_watch_run.rb +82 -0
- metadata +92 -42
- data/LICENSE +0 -20
- data/README.md +0 -269
- data/Rakefile +0 -19
- data/bin/filewatcher +0 -168
- data/test/fixtures/file1.txt +0 -1
- data/test/fixtures/file2.txt +0 -1
- data/test/fixtures/file3.rb +0 -1
- data/test/fixtures/file4.rb +0 -1
- data/test/fixtures/subdir/file5.rb +0 -1
- data/test/fixtures/subdir/file6.rb +0 -1
- data/test/test_filewatcher.rb +0 -181
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8bff81f17e2979e1f38b9a19ed820827b4f7856b018adab83f212cb7a9777011
|
4
|
+
data.tar.gz: 6ce4ec41d01b08e4be1839d7f4fc834a66403517b0430a458ccff814ef61fc23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d58c98c188d13e760d644964d25c21edbe8d863646825fe703f08e63b7ffd58b1a702fd284f7d66aa785a939c626a5d79ccc5aa4e6abd15b2e014490d9d467f
|
7
|
+
data.tar.gz: 6a66f86cc5a4357ddf0182fdf15088f6f1935430643c47e1caef509c13795cdb181d8433b213dcd76090188ca1782f5d94557c4ff59c9ed9d27a8584ca6ff6cd
|
data/lib/filewatcher.rb
CHANGED
@@ -1,189 +1,130 @@
|
|
1
|
-
#
|
2
|
-
# Simple file watcher. Detect changes in files and directories.
|
3
|
-
#
|
4
|
-
# Issues: Currently doesn't monitor changes in directorynames
|
5
|
-
class FileWatcher
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
3
|
+
require 'logger'
|
4
|
+
require_relative 'filewatcher/cycles'
|
5
|
+
require_relative 'filewatcher/snapshots'
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Simple file watcher. Detect changes in files and directories.
|
8
|
+
#
|
9
|
+
# Issues: Currently doesn't monitor changes in directory names
|
10
|
+
class Filewatcher
|
11
|
+
include Filewatcher::Cycles
|
12
|
+
include Filewatcher::Snapshots
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
@spinner ||= %w(\\ | / -)
|
16
|
-
print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
|
17
|
-
end
|
14
|
+
attr_accessor :interval
|
15
|
+
attr_reader :keep_watching
|
18
16
|
|
19
|
-
def initialize(unexpanded_filenames,
|
20
|
-
if(args.first)
|
21
|
-
options = args.first
|
22
|
-
else
|
23
|
-
options = {}
|
24
|
-
end
|
17
|
+
def initialize(unexpanded_filenames, options = {})
|
25
18
|
@unexpanded_filenames = unexpanded_filenames
|
26
19
|
@unexpanded_excluded_filenames = options[:exclude]
|
27
|
-
@filenames = nil
|
28
|
-
@stored_update = nil
|
29
20
|
@keep_watching = false
|
30
21
|
@pausing = false
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@
|
34
|
-
|
35
|
-
|
22
|
+
@immediate = options[:immediate]
|
23
|
+
@interval = options.fetch(:interval, 0.5)
|
24
|
+
@logger = options.fetch(:logger, Logger.new($stdout, level: :info))
|
25
|
+
|
26
|
+
after_initialize unexpanded_filenames, options
|
36
27
|
end
|
37
28
|
|
38
|
-
def watch(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
29
|
+
def watch(&on_update)
|
30
|
+
## The set of available signals depends on the OS
|
31
|
+
## Windows doesn't support `HUP` signal, for example
|
32
|
+
(%w[HUP INT TERM] & Signal.list.keys).each do |signal|
|
33
|
+
trap(signal) { exit }
|
43
34
|
end
|
44
|
-
|
35
|
+
|
36
|
+
@on_update = on_update
|
45
37
|
@keep_watching = true
|
46
|
-
if
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
while @keep_watching && @pausing
|
52
|
-
update_spinner('Pausing')
|
53
|
-
Kernel.sleep @sleep
|
54
|
-
end
|
55
|
-
while @keep_watching && !filesystem_updated? && !@pausing
|
56
|
-
update_spinner('Watching')
|
57
|
-
Kernel.sleep @sleep
|
58
|
-
end
|
59
|
-
# test and null @updated_file to prevent yielding the last
|
60
|
-
# file twice if @keep_watching has just been set to false
|
61
|
-
yield @updated_file, @event if @updated_file
|
62
|
-
@updated_file = nil
|
63
|
-
end
|
64
|
-
@end_snapshot = mtime_snapshot
|
38
|
+
yield({ '' => '' }) if @immediate
|
39
|
+
|
40
|
+
main_cycle
|
41
|
+
|
42
|
+
@end_snapshot = current_snapshot
|
65
43
|
finalize(&on_update)
|
66
44
|
end
|
67
45
|
|
68
46
|
def pause
|
69
47
|
@pausing = true
|
70
|
-
|
71
|
-
|
72
|
-
|
48
|
+
|
49
|
+
before_pause_sleep
|
50
|
+
|
51
|
+
# Ensure we wait long enough to enter pause loop in #watch
|
52
|
+
sleep @interval
|
73
53
|
end
|
74
54
|
|
75
55
|
def resume
|
76
|
-
if !@keep_watching || !@pausing
|
77
|
-
|
78
|
-
|
79
|
-
@last_snapshot = mtime_snapshot # resume with fresh snapshot
|
56
|
+
raise "Can't resume unless #watch and #pause were first called" if !@keep_watching || !@pausing
|
57
|
+
|
58
|
+
@last_snapshot = current_snapshot # resume with fresh snapshot
|
80
59
|
@pausing = false
|
81
|
-
|
82
|
-
|
60
|
+
|
61
|
+
before_resume_sleep
|
62
|
+
|
63
|
+
sleep @interval # Wait long enough to exit pause loop in #watch
|
83
64
|
end
|
84
65
|
|
85
66
|
# Ends the watch, allowing any remaining changes to be finalized.
|
86
67
|
# Used mainly in multi-threaded situations.
|
87
68
|
def stop
|
88
69
|
@keep_watching = false
|
89
|
-
|
90
|
-
|
70
|
+
|
71
|
+
after_stop
|
72
|
+
|
73
|
+
nil
|
91
74
|
end
|
92
75
|
|
93
76
|
# Calls the update block repeatedly until all changes in the
|
94
77
|
# current snapshot are dealt with
|
95
78
|
def finalize(&on_update)
|
96
|
-
on_update = @
|
97
|
-
snapshot = @end_snapshot ? @end_snapshot : mtime_snapshot
|
98
|
-
while filesystem_updated?(snapshot)
|
99
|
-
update_spinner('Finalizing')
|
100
|
-
on_update.call(@updated_file, @event)
|
101
|
-
end
|
102
|
-
@end_snapshot =nil
|
103
|
-
return nil
|
104
|
-
end
|
79
|
+
on_update = @on_update unless block_given?
|
105
80
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
snapshot = {}
|
110
|
-
@filenames = expand_directories(@unexpanded_filenames)
|
111
|
-
|
112
|
-
if(@unexpanded_excluded_filenames != nil and @unexpanded_excluded_filenames.size > 0)
|
113
|
-
# Remove files in the exclude filenames list
|
114
|
-
@filtered_filenames = []
|
115
|
-
@excluded_filenames = expand_directories(@unexpanded_excluded_filenames)
|
116
|
-
@filenames.each do |filename|
|
117
|
-
if(not(@excluded_filenames.include?(filename)))
|
118
|
-
@filtered_filenames << filename
|
119
|
-
end
|
120
|
-
end
|
121
|
-
@filenames = @filtered_filenames
|
81
|
+
while file_system_updated?(@end_snapshot || current_snapshot)
|
82
|
+
finalizing
|
83
|
+
trigger_changes(on_update)
|
122
84
|
end
|
123
85
|
|
124
|
-
@
|
125
|
-
mtime = File.exist?(filename) ? File.stat(filename).mtime : Time.new(0)
|
126
|
-
snapshot[filename] = mtime
|
127
|
-
end
|
128
|
-
return snapshot
|
86
|
+
@end_snapshot = nil
|
129
87
|
end
|
130
88
|
|
131
|
-
|
132
|
-
snapshot = snapshot_to_use ? snapshot_to_use : mtime_snapshot
|
133
|
-
forward_changes = snapshot.to_a - @last_snapshot.to_a
|
134
|
-
|
135
|
-
forward_changes.each do |file,mtime|
|
136
|
-
@updated_file = file
|
137
|
-
unless @last_snapshot.fetch(@updated_file,false)
|
138
|
-
@last_snapshot[file] = mtime
|
139
|
-
@event = :new
|
140
|
-
return true
|
141
|
-
else
|
142
|
-
@last_snapshot[file] = mtime
|
143
|
-
@event = :changed
|
144
|
-
return true
|
145
|
-
end
|
146
|
-
end
|
89
|
+
private
|
147
90
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
return true
|
91
|
+
def expand_directories(patterns)
|
92
|
+
patterns = Array(patterns) unless patterns.is_a? Array
|
93
|
+
expanded_patterns = patterns.map do |pattern|
|
94
|
+
pattern = File.expand_path(pattern)
|
95
|
+
Dir[
|
96
|
+
File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
|
97
|
+
]
|
156
98
|
end
|
157
|
-
|
99
|
+
expanded_patterns.flatten!
|
100
|
+
expanded_patterns.uniq!
|
101
|
+
expanded_patterns
|
158
102
|
end
|
159
103
|
|
160
|
-
def
|
161
|
-
@
|
104
|
+
def debug(data)
|
105
|
+
@logger.debug "Thread ##{Thread.current.object_id} #{data}"
|
162
106
|
end
|
163
107
|
|
164
|
-
def
|
165
|
-
if(
|
166
|
-
patterns = [patterns]
|
167
|
-
end
|
168
|
-
patterns.map { |it| Dir[fulldepth(expand_path(it))] }.flatten.uniq
|
108
|
+
def after_initialize(*)
|
109
|
+
super if defined?(super)
|
169
110
|
end
|
170
111
|
|
171
|
-
|
112
|
+
def before_pause_sleep
|
113
|
+
super if defined?(super)
|
114
|
+
end
|
172
115
|
|
173
|
-
def
|
174
|
-
if
|
175
|
-
"#{pattern}/**/*"
|
176
|
-
else
|
177
|
-
pattern
|
178
|
-
end
|
116
|
+
def before_resume_sleep
|
117
|
+
super if defined?(super)
|
179
118
|
end
|
180
119
|
|
181
|
-
def
|
182
|
-
if
|
183
|
-
File.expand_path(pattern)
|
184
|
-
else
|
185
|
-
pattern
|
186
|
-
end
|
120
|
+
def after_stop
|
121
|
+
super if defined?(super)
|
187
122
|
end
|
188
123
|
|
124
|
+
def finalizing
|
125
|
+
super if defined?(super)
|
126
|
+
end
|
189
127
|
end
|
128
|
+
|
129
|
+
# Require at end of file to not overwrite `Filewatcher` class
|
130
|
+
require_relative 'filewatcher/version'
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Filewatcher
|
4
|
+
# Module for all cycles in `Filewatcher#watch`
|
5
|
+
module Cycles
|
6
|
+
private
|
7
|
+
|
8
|
+
def main_cycle
|
9
|
+
while @keep_watching
|
10
|
+
@end_snapshot = current_snapshot if @pausing
|
11
|
+
|
12
|
+
pausing_cycle
|
13
|
+
|
14
|
+
watching_cycle
|
15
|
+
|
16
|
+
# test and clear @changes to prevent yielding the last
|
17
|
+
# changes twice if @keep_watching has just been set to false
|
18
|
+
trigger_changes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def pausing_cycle
|
23
|
+
while @keep_watching && @pausing
|
24
|
+
before_pausing_sleep
|
25
|
+
|
26
|
+
sleep @interval
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def before_pausing_sleep
|
31
|
+
super if defined?(super)
|
32
|
+
end
|
33
|
+
|
34
|
+
def watching_cycle
|
35
|
+
@last_snapshot ||= current_snapshot
|
36
|
+
loop do
|
37
|
+
before_watching_sleep
|
38
|
+
|
39
|
+
debug "#{__method__} sleep #{@interval}"
|
40
|
+
sleep @interval
|
41
|
+
break if !@keep_watching || file_system_updated? || @pausing
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def before_watching_sleep
|
46
|
+
super if defined?(super)
|
47
|
+
end
|
48
|
+
|
49
|
+
def trigger_changes(on_update = @on_update)
|
50
|
+
debug __method__
|
51
|
+
on_update.call(@changes.dup) unless @changes.empty?
|
52
|
+
@changes.clear
|
53
|
+
debug '@changes cleared'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
class Filewatcher
|
6
|
+
# Class for snapshots of file system
|
7
|
+
class Snapshot
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@data, :[], :each, :keys
|
10
|
+
|
11
|
+
def initialize(filenames)
|
12
|
+
@data = filenames.each_with_object({}) do |filename, data|
|
13
|
+
data[filename] = SnapshotFile.new(filename)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def -(other)
|
18
|
+
changes = {}
|
19
|
+
|
20
|
+
each do |filename, snapshot_file|
|
21
|
+
changes[filename] = snapshot_file - other[filename]
|
22
|
+
end
|
23
|
+
|
24
|
+
other.each do |filename, _snapshot_file|
|
25
|
+
changes[filename] = :deleted unless self[filename]
|
26
|
+
end
|
27
|
+
|
28
|
+
changes.reject! { |_filename, event| event.nil? }
|
29
|
+
changes
|
30
|
+
end
|
31
|
+
|
32
|
+
# Class for one file from snapshot
|
33
|
+
class SnapshotFile
|
34
|
+
STATS = %i[mtime].freeze
|
35
|
+
|
36
|
+
attr_reader(*STATS)
|
37
|
+
|
38
|
+
def initialize(filename)
|
39
|
+
@filename = filename
|
40
|
+
STATS.each do |stat|
|
41
|
+
time = File.public_send(stat, filename) if File.exist?(filename)
|
42
|
+
instance_variable_set :"@#{stat}", time || Time.new(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def -(other)
|
47
|
+
if other.nil?
|
48
|
+
:created
|
49
|
+
elsif other.mtime < mtime
|
50
|
+
:updated
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
<<~OUTPUT
|
56
|
+
#<Filewatcher::Snapshot::SnapshotFile:#{object_id}
|
57
|
+
@filename=#{@filename.inspect}, mtime=#{mtime.strftime('%F %T.%9N').inspect}
|
58
|
+
>
|
59
|
+
OUTPUT
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private_constant :SnapshotFile
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'snapshot'
|
4
|
+
|
5
|
+
# Helpers in Filewatcher class itself
|
6
|
+
class Filewatcher
|
7
|
+
class << self
|
8
|
+
def system_stat(filename)
|
9
|
+
case Gem::Platform.local.os
|
10
|
+
when 'linux' then `stat --printf 'Modification: %y, Change: %z\n' #{filename}`
|
11
|
+
when 'darwin' then `stat #{filename}`
|
12
|
+
else 'Unknown OS for system `stat`'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Module for snapshot logic inside Filewatcher
|
18
|
+
module Snapshots
|
19
|
+
def found_filenames
|
20
|
+
current_snapshot.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def watching_files
|
26
|
+
expand_directories(@unexpanded_filenames) - expand_directories(@unexpanded_excluded_filenames)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Takes a snapshot of the current status of watched files.
|
30
|
+
# (Allows avoidance of potential race condition during #finalize)
|
31
|
+
def current_snapshot
|
32
|
+
Filewatcher::Snapshot.new(watching_files)
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_mtime(filename)
|
36
|
+
return Time.new(0) unless File.exist?(filename)
|
37
|
+
|
38
|
+
result = File.mtime(filename)
|
39
|
+
if @logger.level <= Logger::DEBUG
|
40
|
+
debug "File.mtime = #{result.strftime('%F %T.%9N')}"
|
41
|
+
debug "stat #{filename}: #{self.class.system_stat(filename)}"
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def file_system_updated?(snapshot = current_snapshot)
|
47
|
+
debug __method__
|
48
|
+
|
49
|
+
@changes = snapshot - @last_snapshot
|
50
|
+
|
51
|
+
@last_snapshot = snapshot
|
52
|
+
|
53
|
+
@changes.any?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|