filewatcher 1.1.1 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 805761615b226e14eb6431116a32bdc34e8b13cf
4
- data.tar.gz: 4c1523220b4c242d33750e145091e1e2568766ac
2
+ SHA256:
3
+ metadata.gz: 8bff81f17e2979e1f38b9a19ed820827b4f7856b018adab83f212cb7a9777011
4
+ data.tar.gz: 6ce4ec41d01b08e4be1839d7f4fc834a66403517b0430a458ccff814ef61fc23
5
5
  SHA512:
6
- metadata.gz: 6d6f196a0ce277f920732a6c4ff0071b82e5b6eb3cf0683834041e973bb3a10a6c914aba6495d0bbbdfd83d241f983f697b45aa8ddd62ad9c973e3b4294fbffa
7
- data.tar.gz: 7a75c0bb5e79efa44207f20052f8907790e2292195f65336f3d06303a8618ebe84fc658e32fec8333da461d7ccc979c74df061c735692ff7dd2647fac70b0f91
6
+ metadata.gz: 3d58c98c188d13e760d644964d25c21edbe8d863646825fe703f08e63b7ffd58b1a702fd284f7d66aa785a939c626a5d79ccc5aa4e6abd15b2e014490d9d467f
7
+ data.tar.gz: 6a66f86cc5a4357ddf0182fdf15088f6f1935430643c47e1caef509c13795cdb181d8433b213dcd76090188ca1782f5d94557c4ff59c9ed9d27a8584ca6ff6cd
@@ -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 directorynames
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
- @every = options[:every]
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('', '') if @immediate
38
+ yield({ '' => '' }) if @immediate
41
39
 
42
40
  main_cycle
43
41
 
44
- @end_snapshot = mtime_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
- update_spinner('Initiating pause')
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
- raise "Can't resume unless #watch and #pause were first called"
58
- end
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
- update_spinner('Resuming')
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
- update_spinner('Stopping')
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
- while filesystem_updated?(@end_snapshot || mtime_snapshot)
78
- update_spinner('Finalizing')
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
- def last_found_filenames
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
@@ -7,7 +7,7 @@ class Filewatcher
7
7
 
8
8
  def main_cycle
9
9
  while @keep_watching
10
- @end_snapshot = mtime_snapshot if @pausing
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
- update_spinner('Pausing')
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
- while @keep_watching && !filesystem_updated? && !@pausing
31
- update_spinner('Watching')
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
- thread = Thread.new do
38
- changes = @every ? @changes : @changes.first(1)
39
- changes.each do |filename, event|
40
- on_update.call(filename, event)
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