filewatcher 1.1.1 → 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 +49 -59
- data/lib/filewatcher/cycles.rb +21 -12
- 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 +1 -3
- 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 +80 -46
- data/bin/banner.txt +0 -17
- data/bin/filewatcher +0 -106
- data/lib/filewatcher/env.rb +0 -33
- data/lib/filewatcher/runner.rb +0 -33
- data/test/dumpers/env_dumper.rb +0 -10
- data/test/dumpers/watched_dumper.rb +0 -5
- data/test/filewatcher/test_env.rb +0 -70
- data/test/filewatcher/test_runner.rb +0 -75
- data/test/filewatcher/test_version.rb +0 -13
- data/test/helper.rb +0 -238
- data/test/test_filewatcher.rb +0 -354
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,31 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'logger'
|
3
4
|
require_relative 'filewatcher/cycles'
|
5
|
+
require_relative 'filewatcher/snapshots'
|
4
6
|
|
5
7
|
# Simple file watcher. Detect changes in files and directories.
|
6
8
|
#
|
7
|
-
# Issues: Currently doesn't monitor changes in
|
9
|
+
# Issues: Currently doesn't monitor changes in directory names
|
8
10
|
class Filewatcher
|
9
11
|
include Filewatcher::Cycles
|
12
|
+
include Filewatcher::Snapshots
|
10
13
|
|
11
14
|
attr_accessor :interval
|
12
15
|
attr_reader :keep_watching
|
13
16
|
|
14
|
-
def update_spinner(label)
|
15
|
-
return unless @show_spinner
|
16
|
-
@spinner ||= %w[\\ | / -]
|
17
|
-
print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
|
18
|
-
end
|
19
|
-
|
20
17
|
def initialize(unexpanded_filenames, options = {})
|
21
18
|
@unexpanded_filenames = unexpanded_filenames
|
22
19
|
@unexpanded_excluded_filenames = options[:exclude]
|
23
20
|
@keep_watching = false
|
24
21
|
@pausing = false
|
25
22
|
@immediate = options[:immediate]
|
26
|
-
@show_spinner = options[:spinner]
|
27
23
|
@interval = options.fetch(:interval, 0.5)
|
28
|
-
@
|
24
|
+
@logger = options.fetch(:logger, Logger.new($stdout, level: :info))
|
25
|
+
|
26
|
+
after_initialize unexpanded_filenames, options
|
29
27
|
end
|
30
28
|
|
31
29
|
def watch(&on_update)
|
@@ -37,28 +35,31 @@ class Filewatcher
|
|
37
35
|
|
38
36
|
@on_update = on_update
|
39
37
|
@keep_watching = true
|
40
|
-
yield(''
|
38
|
+
yield({ '' => '' }) if @immediate
|
41
39
|
|
42
40
|
main_cycle
|
43
41
|
|
44
|
-
@end_snapshot =
|
42
|
+
@end_snapshot = current_snapshot
|
45
43
|
finalize(&on_update)
|
46
44
|
end
|
47
45
|
|
48
46
|
def pause
|
49
47
|
@pausing = true
|
50
|
-
|
48
|
+
|
49
|
+
before_pause_sleep
|
50
|
+
|
51
51
|
# Ensure we wait long enough to enter pause loop in #watch
|
52
52
|
sleep @interval
|
53
53
|
end
|
54
54
|
|
55
55
|
def resume
|
56
|
-
if !@keep_watching || !@pausing
|
57
|
-
|
58
|
-
|
59
|
-
@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
|
60
59
|
@pausing = false
|
61
|
-
|
60
|
+
|
61
|
+
before_resume_sleep
|
62
|
+
|
62
63
|
sleep @interval # Wait long enough to exit pause loop in #watch
|
63
64
|
end
|
64
65
|
|
@@ -66,7 +67,9 @@ class Filewatcher
|
|
66
67
|
# Used mainly in multi-threaded situations.
|
67
68
|
def stop
|
68
69
|
@keep_watching = false
|
69
|
-
|
70
|
+
|
71
|
+
after_stop
|
72
|
+
|
70
73
|
nil
|
71
74
|
end
|
72
75
|
|
@@ -74,54 +77,17 @@ class Filewatcher
|
|
74
77
|
# current snapshot are dealt with
|
75
78
|
def finalize(&on_update)
|
76
79
|
on_update = @on_update unless block_given?
|
77
|
-
|
78
|
-
|
80
|
+
|
81
|
+
while file_system_updated?(@end_snapshot || current_snapshot)
|
82
|
+
finalizing
|
79
83
|
trigger_changes(on_update)
|
80
84
|
end
|
81
|
-
@end_snapshot = nil
|
82
|
-
end
|
83
85
|
|
84
|
-
|
85
|
-
last_snapshot.keys
|
86
|
+
@end_snapshot = nil
|
86
87
|
end
|
87
88
|
|
88
89
|
private
|
89
90
|
|
90
|
-
def last_snapshot
|
91
|
-
@last_snapshot ||= mtime_snapshot
|
92
|
-
end
|
93
|
-
|
94
|
-
# Takes a snapshot of the current status of watched files.
|
95
|
-
# (Allows avoidance of potential race condition during #finalize)
|
96
|
-
def mtime_snapshot
|
97
|
-
snapshot = {}
|
98
|
-
filenames = expand_directories(@unexpanded_filenames)
|
99
|
-
|
100
|
-
# Remove files in the exclude filenames list
|
101
|
-
filenames -= expand_directories(@unexpanded_excluded_filenames)
|
102
|
-
|
103
|
-
filenames.each do |filename|
|
104
|
-
mtime = File.exist?(filename) ? File.mtime(filename) : Time.new(0)
|
105
|
-
snapshot[filename] = mtime
|
106
|
-
end
|
107
|
-
snapshot
|
108
|
-
end
|
109
|
-
|
110
|
-
def filesystem_updated?(snapshot = mtime_snapshot)
|
111
|
-
@changes = {}
|
112
|
-
|
113
|
-
(snapshot.to_a - last_snapshot.to_a).each do |file, _mtime|
|
114
|
-
@changes[file] = last_snapshot[file] ? :updated : :created
|
115
|
-
end
|
116
|
-
|
117
|
-
(last_snapshot.keys - snapshot.keys).each do |file|
|
118
|
-
@changes[file] = :deleted
|
119
|
-
end
|
120
|
-
|
121
|
-
@last_snapshot = snapshot
|
122
|
-
@changes.any?
|
123
|
-
end
|
124
|
-
|
125
91
|
def expand_directories(patterns)
|
126
92
|
patterns = Array(patterns) unless patterns.is_a? Array
|
127
93
|
expanded_patterns = patterns.map do |pattern|
|
@@ -134,6 +100,30 @@ class Filewatcher
|
|
134
100
|
expanded_patterns.uniq!
|
135
101
|
expanded_patterns
|
136
102
|
end
|
103
|
+
|
104
|
+
def debug(data)
|
105
|
+
@logger.debug "Thread ##{Thread.current.object_id} #{data}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def after_initialize(*)
|
109
|
+
super if defined?(super)
|
110
|
+
end
|
111
|
+
|
112
|
+
def before_pause_sleep
|
113
|
+
super if defined?(super)
|
114
|
+
end
|
115
|
+
|
116
|
+
def before_resume_sleep
|
117
|
+
super if defined?(super)
|
118
|
+
end
|
119
|
+
|
120
|
+
def after_stop
|
121
|
+
super if defined?(super)
|
122
|
+
end
|
123
|
+
|
124
|
+
def finalizing
|
125
|
+
super if defined?(super)
|
126
|
+
end
|
137
127
|
end
|
138
128
|
|
139
129
|
# Require at end of file to not overwrite `Filewatcher` class
|
data/lib/filewatcher/cycles.rb
CHANGED
@@ -7,7 +7,7 @@ class Filewatcher
|
|
7
7
|
|
8
8
|
def main_cycle
|
9
9
|
while @keep_watching
|
10
|
-
@end_snapshot =
|
10
|
+
@end_snapshot = current_snapshot if @pausing
|
11
11
|
|
12
12
|
pausing_cycle
|
13
13
|
|
@@ -21,27 +21,36 @@ class Filewatcher
|
|
21
21
|
|
22
22
|
def pausing_cycle
|
23
23
|
while @keep_watching && @pausing
|
24
|
-
|
24
|
+
before_pausing_sleep
|
25
|
+
|
25
26
|
sleep @interval
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
30
|
+
def before_pausing_sleep
|
31
|
+
super if defined?(super)
|
32
|
+
end
|
33
|
+
|
29
34
|
def watching_cycle
|
30
|
-
|
31
|
-
|
35
|
+
@last_snapshot ||= current_snapshot
|
36
|
+
loop do
|
37
|
+
before_watching_sleep
|
38
|
+
|
39
|
+
debug "#{__method__} sleep #{@interval}"
|
32
40
|
sleep @interval
|
41
|
+
break if !@keep_watching || file_system_updated? || @pausing
|
33
42
|
end
|
34
43
|
end
|
35
44
|
|
45
|
+
def before_watching_sleep
|
46
|
+
super if defined?(super)
|
47
|
+
end
|
48
|
+
|
36
49
|
def trigger_changes(on_update = @on_update)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
@changes.clear
|
43
|
-
end
|
44
|
-
thread.join
|
50
|
+
debug __method__
|
51
|
+
on_update.call(@changes.dup) unless @changes.empty?
|
52
|
+
@changes.clear
|
53
|
+
debug '@changes cleared'
|
45
54
|
end
|
46
55
|
end
|
47
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
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'pry-byebug'
|
7
|
+
rescue LoadError
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'spec_helper/watch_run'
|
12
|
+
|
13
|
+
class Filewatcher
|
14
|
+
## Helper for common spec features between plugins
|
15
|
+
module SpecHelper
|
16
|
+
def logger
|
17
|
+
@logger ||= Logger.new($stdout, level: :debug)
|
18
|
+
end
|
19
|
+
|
20
|
+
def environment_specs_coefficients
|
21
|
+
@environment_specs_coefficients ||= {
|
22
|
+
-> { ENV['CI'] } => 1,
|
23
|
+
-> { RUBY_PLATFORM == 'java' } => 3,
|
24
|
+
-> { Gem::Platform.local.os == 'darwin' } => 1
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait(seconds: 1, interval: 1, &block)
|
29
|
+
environment_specs_coefficients.each do |condition, coefficient|
|
30
|
+
next unless instance_exec(&condition)
|
31
|
+
|
32
|
+
interval *= coefficient
|
33
|
+
seconds *= coefficient
|
34
|
+
end
|
35
|
+
|
36
|
+
if block_given?
|
37
|
+
wait_with_block seconds, interval, &block
|
38
|
+
else
|
39
|
+
wait_without_block seconds
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def wait_with_block(seconds, interval, &_block)
|
44
|
+
(seconds / interval).ceil.times do
|
45
|
+
break if yield
|
46
|
+
|
47
|
+
debug "sleep interval #{interval}"
|
48
|
+
sleep interval
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def wait_without_block(seconds)
|
53
|
+
debug "sleep without intervals #{seconds}"
|
54
|
+
sleep seconds
|
55
|
+
end
|
56
|
+
|
57
|
+
def debug(string)
|
58
|
+
logger.debug "Thread ##{Thread.current.object_id} #{string}"
|
59
|
+
end
|
60
|
+
|
61
|
+
## https://github.com/rubocop-hq/ruby-style-guide/issues/556#issuecomment-691274359
|
62
|
+
# rubocop:disable Style/ModuleFunction
|
63
|
+
extend self
|
64
|
+
# rubocop:enable Style/ModuleFunction
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Filewatcher
|
4
|
+
module SpecHelper
|
5
|
+
## Base class for Filewatcher runners in specs
|
6
|
+
class WatchRun
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
TMP_DIR = "#{Dir.getwd}/spec/tmp"
|
10
|
+
|
11
|
+
def_delegators Filewatcher::SpecHelper, :debug, :wait
|
12
|
+
|
13
|
+
attr_reader :filename
|
14
|
+
|
15
|
+
def initialize(filename:, action:, directory:)
|
16
|
+
@filename =
|
17
|
+
if filename.match? %r{^(/|~|[A-Z]:)} then filename
|
18
|
+
else File.join(TMP_DIR, filename)
|
19
|
+
end
|
20
|
+
@directory = directory
|
21
|
+
@action = action
|
22
|
+
debug "action = #{action}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
debug 'start'
|
27
|
+
File.write(@filename, 'content1') unless @action == :create
|
28
|
+
|
29
|
+
wait seconds: 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def run(make_changes_times: 1)
|
33
|
+
start
|
34
|
+
|
35
|
+
make_changes_times.times do
|
36
|
+
make_changes
|
37
|
+
|
38
|
+
wait seconds: 2
|
39
|
+
end
|
40
|
+
|
41
|
+
stop
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
debug 'stop'
|
46
|
+
FileUtils.rm_r(@filename) if File.exist?(@filename)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def make_changes
|
52
|
+
debug "make changes, @action = #{@action}, @filename = #{@filename}"
|
53
|
+
|
54
|
+
if @action == :delete
|
55
|
+
FileUtils.remove(@filename)
|
56
|
+
elsif @directory
|
57
|
+
FileUtils.mkdir_p(@filename)
|
58
|
+
else
|
59
|
+
## There is no `File.write` because of strange difference in parallel `File.mtime`
|
60
|
+
## https://cirrus-ci.com/task/6107605053472768?command=test#L497-L511
|
61
|
+
system "echo 'content2' > #{@filename}"
|
62
|
+
debug_file_mtime
|
63
|
+
end
|
64
|
+
|
65
|
+
wait seconds: 1
|
66
|
+
end
|
67
|
+
|
68
|
+
def debug_file_mtime
|
69
|
+
debug "stat #{@filename}: #{Filewatcher.system_stat(@filename)}"
|
70
|
+
debug "File.mtime = #{File.mtime(@filename).strftime('%F %T.%9N')}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|