filewatcher 1.1.1 → 2.0.0.beta5
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 +46 -60
- data/lib/filewatcher/cycles.rb +21 -12
- data/lib/filewatcher/snapshot.rb +64 -0
- data/lib/filewatcher/snapshots.rb +47 -0
- data/lib/filewatcher/spec_helper.rb +89 -0
- data/lib/filewatcher/spec_helper/watch_run.rb +72 -0
- data/lib/filewatcher/version.rb +1 -3
- data/spec/filewatcher/snapshot_spec.rb +68 -0
- data/spec/filewatcher/version_spec.rb +9 -0
- data/spec/filewatcher_spec.rb +302 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/spec_helper/ruby_watch_run.rb +82 -0
- metadata +110 -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: c4552a19cac521012fb0cca2c232a14f501530262e02639ed017c0897e5f985f
|
4
|
+
data.tar.gz: f3d98c14badcbd439a9dca5ce289b40cc80aecb1c377d8ec08f80e76c398aaf5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85e030c3e12237aff4dffddfb80fdf4b288b462a35e21a63d254e1aea4ca40cd2d5ba27cb21e39e3847f16936d4291398a106bd55a3ef40205f35d75c401603c
|
7
|
+
data.tar.gz: 7cec237639d7c760b963fe42b7c7d6ef40b989dde617308434e7d29037f73aad3f6fc87f971f60b4e47f420aec779e36ab76e5a6a2f27cf4f1c0849719b0b286
|
data/lib/filewatcher.rb
CHANGED
@@ -1,20 +1,26 @@
|
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
class << self
|
18
|
+
def print_version
|
19
|
+
system 'ruby -v'
|
20
|
+
puts "Filewatcher #{self::VERSION}"
|
21
|
+
|
22
|
+
super if defined? super
|
23
|
+
end
|
18
24
|
end
|
19
25
|
|
20
26
|
def initialize(unexpanded_filenames, options = {})
|
@@ -23,9 +29,10 @@ class Filewatcher
|
|
23
29
|
@keep_watching = false
|
24
30
|
@pausing = false
|
25
31
|
@immediate = options[:immediate]
|
26
|
-
@show_spinner = options[:spinner]
|
27
32
|
@interval = options.fetch(:interval, 0.5)
|
28
|
-
@
|
33
|
+
@logger = options.fetch(:logger, Logger.new($stdout, level: :info))
|
34
|
+
|
35
|
+
after_initialize unexpanded_filenames, options
|
29
36
|
end
|
30
37
|
|
31
38
|
def watch(&on_update)
|
@@ -37,28 +44,31 @@ class Filewatcher
|
|
37
44
|
|
38
45
|
@on_update = on_update
|
39
46
|
@keep_watching = true
|
40
|
-
yield(''
|
47
|
+
yield({ '' => '' }) if @immediate
|
41
48
|
|
42
49
|
main_cycle
|
43
50
|
|
44
|
-
@end_snapshot =
|
51
|
+
@end_snapshot = current_snapshot
|
45
52
|
finalize(&on_update)
|
46
53
|
end
|
47
54
|
|
48
55
|
def pause
|
49
56
|
@pausing = true
|
50
|
-
|
57
|
+
|
58
|
+
before_pause_sleep
|
59
|
+
|
51
60
|
# Ensure we wait long enough to enter pause loop in #watch
|
52
61
|
sleep @interval
|
53
62
|
end
|
54
63
|
|
55
64
|
def resume
|
56
|
-
if !@keep_watching || !@pausing
|
57
|
-
|
58
|
-
|
59
|
-
@last_snapshot = mtime_snapshot # resume with fresh snapshot
|
65
|
+
raise "Can't resume unless #watch and #pause were first called" if !@keep_watching || !@pausing
|
66
|
+
|
67
|
+
@last_snapshot = current_snapshot # resume with fresh snapshot
|
60
68
|
@pausing = false
|
61
|
-
|
69
|
+
|
70
|
+
before_resume_sleep
|
71
|
+
|
62
72
|
sleep @interval # Wait long enough to exit pause loop in #watch
|
63
73
|
end
|
64
74
|
|
@@ -66,7 +76,9 @@ class Filewatcher
|
|
66
76
|
# Used mainly in multi-threaded situations.
|
67
77
|
def stop
|
68
78
|
@keep_watching = false
|
69
|
-
|
79
|
+
|
80
|
+
after_stop
|
81
|
+
|
70
82
|
nil
|
71
83
|
end
|
72
84
|
|
@@ -74,65 +86,39 @@ class Filewatcher
|
|
74
86
|
# current snapshot are dealt with
|
75
87
|
def finalize(&on_update)
|
76
88
|
on_update = @on_update unless block_given?
|
77
|
-
|
78
|
-
|
89
|
+
|
90
|
+
while file_system_updated?(@end_snapshot || current_snapshot)
|
91
|
+
finalizing
|
79
92
|
trigger_changes(on_update)
|
80
93
|
end
|
81
|
-
@end_snapshot = nil
|
82
|
-
end
|
83
94
|
|
84
|
-
|
85
|
-
last_snapshot.keys
|
95
|
+
@end_snapshot = nil
|
86
96
|
end
|
87
97
|
|
88
98
|
private
|
89
99
|
|
90
|
-
def
|
91
|
-
@
|
100
|
+
def debug(data)
|
101
|
+
@logger.debug "Thread ##{Thread.current.object_id} #{data}"
|
92
102
|
end
|
93
103
|
|
94
|
-
|
95
|
-
|
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
|
104
|
+
def after_initialize(*)
|
105
|
+
super if defined?(super)
|
108
106
|
end
|
109
107
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
(snapshot.to_a - last_snapshot.to_a).each do |file, _mtime|
|
114
|
-
@changes[file] = last_snapshot[file] ? :updated : :created
|
115
|
-
end
|
108
|
+
def before_pause_sleep
|
109
|
+
super if defined?(super)
|
110
|
+
end
|
116
111
|
|
117
|
-
|
118
|
-
|
119
|
-
|
112
|
+
def before_resume_sleep
|
113
|
+
super if defined?(super)
|
114
|
+
end
|
120
115
|
|
121
|
-
|
122
|
-
|
116
|
+
def after_stop
|
117
|
+
super if defined?(super)
|
123
118
|
end
|
124
119
|
|
125
|
-
def
|
126
|
-
|
127
|
-
expanded_patterns = patterns.map do |pattern|
|
128
|
-
pattern = File.expand_path(pattern)
|
129
|
-
Dir[
|
130
|
-
File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
|
131
|
-
]
|
132
|
-
end
|
133
|
-
expanded_patterns.flatten!
|
134
|
-
expanded_patterns.uniq!
|
135
|
-
expanded_patterns
|
120
|
+
def finalizing
|
121
|
+
super if defined?(super)
|
136
122
|
end
|
137
123
|
end
|
138
124
|
|
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,64 @@
|
|
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.tap(&:compact!)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Class for one file from snapshot
|
32
|
+
class SnapshotFile
|
33
|
+
STATS = %i[mtime].freeze
|
34
|
+
|
35
|
+
attr_reader(*STATS)
|
36
|
+
|
37
|
+
def initialize(filename)
|
38
|
+
@filename = filename
|
39
|
+
STATS.each do |stat|
|
40
|
+
time = File.public_send(stat, filename) if File.exist?(filename)
|
41
|
+
instance_variable_set :"@#{stat}", time || Time.new(0)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def -(other)
|
46
|
+
if other.nil?
|
47
|
+
:created
|
48
|
+
elsif other.mtime < mtime
|
49
|
+
:updated
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
<<~OUTPUT
|
55
|
+
#<Filewatcher::Snapshot::SnapshotFile:#{object_id}
|
56
|
+
@filename=#{@filename.inspect}, mtime=#{mtime.strftime('%F %T.%9N').inspect}
|
57
|
+
>
|
58
|
+
OUTPUT
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private_constant :SnapshotFile
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'snapshot'
|
4
|
+
|
5
|
+
# Helpers in Filewatcher class itself
|
6
|
+
class Filewatcher
|
7
|
+
# Module for snapshot logic inside Filewatcher
|
8
|
+
module Snapshots
|
9
|
+
def found_filenames
|
10
|
+
current_snapshot.keys
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Takes a snapshot of the current status of watched files.
|
16
|
+
# (Allows avoidance of potential race condition during #finalize)
|
17
|
+
def current_snapshot
|
18
|
+
Filewatcher::Snapshot.new(
|
19
|
+
expand_directories(@unexpanded_filenames) -
|
20
|
+
expand_directories(@unexpanded_excluded_filenames)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def expand_directories(patterns)
|
25
|
+
patterns = Array(patterns) unless patterns.is_a? Array
|
26
|
+
expanded_patterns = patterns.map do |pattern|
|
27
|
+
pattern = File.expand_path(pattern)
|
28
|
+
Dir[
|
29
|
+
File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
|
30
|
+
]
|
31
|
+
end
|
32
|
+
expanded_patterns.flatten!
|
33
|
+
expanded_patterns.uniq!
|
34
|
+
expanded_patterns
|
35
|
+
end
|
36
|
+
|
37
|
+
def file_system_updated?(snapshot = current_snapshot)
|
38
|
+
debug __method__
|
39
|
+
|
40
|
+
@changes = snapshot - @last_snapshot
|
41
|
+
|
42
|
+
@last_snapshot = snapshot
|
43
|
+
|
44
|
+
@changes.any?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,89 @@
|
|
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
|
+
ENVIRONMENT_SPECS_COEFFICIENTS = {
|
17
|
+
-> { ENV['CI'] } => 1,
|
18
|
+
-> { RUBY_PLATFORM == 'java' } => 1,
|
19
|
+
-> { Gem::Platform.local.os == 'darwin' } => 1
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= Logger.new($stdout, level: :debug)
|
24
|
+
end
|
25
|
+
|
26
|
+
def environment_specs_coefficients
|
27
|
+
ENVIRONMENT_SPECS_COEFFICIENTS
|
28
|
+
end
|
29
|
+
|
30
|
+
def wait(seconds: 1, interval: 1, &block)
|
31
|
+
environment_specs_coefficients.each do |condition, coefficient|
|
32
|
+
next unless instance_exec(&condition)
|
33
|
+
|
34
|
+
interval *= coefficient
|
35
|
+
seconds *= coefficient
|
36
|
+
end
|
37
|
+
|
38
|
+
if block
|
39
|
+
wait_with_block seconds, interval, &block
|
40
|
+
else
|
41
|
+
wait_without_block seconds
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def wait_with_block(seconds, interval, &_block)
|
46
|
+
(seconds / interval).ceil.times do
|
47
|
+
break if yield
|
48
|
+
|
49
|
+
debug "sleep interval #{interval}"
|
50
|
+
sleep interval
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait_without_block(seconds)
|
55
|
+
debug "sleep without intervals #{seconds}"
|
56
|
+
sleep seconds
|
57
|
+
end
|
58
|
+
|
59
|
+
def debug(string)
|
60
|
+
logger.debug "Thread ##{Thread.current.object_id} #{string}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def system_stat(filename)
|
64
|
+
case (host_os = RbConfig::CONFIG['host_os'])
|
65
|
+
when /linux(-gnu)?/
|
66
|
+
`stat --printf 'Modification: %y, Change: %z\n' #{filename}`
|
67
|
+
when /darwin\d*/
|
68
|
+
`stat #{filename}`
|
69
|
+
when *Gem::WIN_PATTERNS
|
70
|
+
system_stat_windows filename
|
71
|
+
else
|
72
|
+
"Unknown OS `#{host_os}` for system's `stat` command"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def system_stat_windows(filename)
|
77
|
+
filename = filename.gsub('/', '\\\\\\')
|
78
|
+
properties = 'CreationDate,InstallDate,LastModified,LastAccessed'
|
79
|
+
command = "wmic datafile where Name=\"#{filename}\" get #{properties}"
|
80
|
+
# debug command
|
81
|
+
`#{command}`
|
82
|
+
end
|
83
|
+
|
84
|
+
## https://github.com/rubocop/ruby-style-guide/issues/556#issuecomment-828672008
|
85
|
+
# rubocop:disable Style/ModuleFunction
|
86
|
+
extend self
|
87
|
+
# rubocop:enable Style/ModuleFunction
|
88
|
+
end
|
89
|
+
end
|