filewatcher 1.0.1 → 2.0.0.beta3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9221be161e73cc2f85ea6f9706a2e558c31ff314664bc31817e9715439a31927
4
- data.tar.gz: bae2bc1ba2f5b88693fe1c8093b135652885094c69adc3da51193916ada96b44
3
+ metadata.gz: e1058d188f32d31bdf63775a8ff6655e64de608669630ef775c0e4975d196421
4
+ data.tar.gz: 57406a8b0be947051f295de3d6208faaf4ec60e432edc22a71d1e3b22fb58415
5
5
  SHA512:
6
- metadata.gz: 0aec2e10e03aad69fd0e4ccda0a6f488efc7c908abe31640319971dda6ed71ca8873588d87693f91309d13ac996bb62a952e3c7430c41b462d663e0064185e8e
7
- data.tar.gz: ebf1581b4e3e56c77c4a18867ce55d2cb9a6fbf97d7804da305192dd9622a39d9b16cfe1392dd880c6dd1c43f47539a66a10f61956de7fad0202f2c43222b64d
6
+ metadata.gz: d930968943fd907aad26eb150de20fa8d2681223dad1f516f9a27c2c096893bacaa8decd0e4646753f529dbec38bba32dd1b6951b442e2df69f6a01f943116fc
7
+ data.tar.gz: cc8919f9f7036cb7599b22589e5b6abadd7b2674f502eae3e9c70c3e29b72d222356f5569244b63b1524dfd64430fc8617283b1bc0f565fffc91b5299b85980e
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 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"
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,37 +29,46 @@ 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
- @every = options[:every]
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)
32
- %w[HUP INT TERM].each { |signal| trap(signal) { exit } }
39
+ ## The set of available signals depends on the OS
40
+ ## Windows doesn't support `HUP` signal, for example
41
+ (%w[HUP INT TERM] & Signal.list.keys).each do |signal|
42
+ trap(signal) { exit }
43
+ end
44
+
33
45
  @on_update = on_update
34
46
  @keep_watching = true
35
- yield('', '') if @immediate
47
+ yield({ '' => '' }) if @immediate
36
48
 
37
49
  main_cycle
38
50
 
39
- @end_snapshot = mtime_snapshot
51
+ @end_snapshot = current_snapshot
40
52
  finalize(&on_update)
41
53
  end
42
54
 
43
55
  def pause
44
56
  @pausing = true
45
- update_spinner('Initiating pause')
57
+
58
+ before_pause_sleep
59
+
46
60
  # Ensure we wait long enough to enter pause loop in #watch
47
61
  sleep @interval
48
62
  end
49
63
 
50
64
  def resume
51
- if !@keep_watching || !@pausing
52
- raise "Can't resume unless #watch and #pause were first called"
53
- end
54
- @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
55
68
  @pausing = false
56
- update_spinner('Resuming')
69
+
70
+ before_resume_sleep
71
+
57
72
  sleep @interval # Wait long enough to exit pause loop in #watch
58
73
  end
59
74
 
@@ -61,7 +76,9 @@ class Filewatcher
61
76
  # Used mainly in multi-threaded situations.
62
77
  def stop
63
78
  @keep_watching = false
64
- update_spinner('Stopping')
79
+
80
+ after_stop
81
+
65
82
  nil
66
83
  end
67
84
 
@@ -69,67 +86,39 @@ class Filewatcher
69
86
  # current snapshot are dealt with
70
87
  def finalize(&on_update)
71
88
  on_update = @on_update unless block_given?
72
- while filesystem_updated?(@end_snapshot || mtime_snapshot)
73
- update_spinner('Finalizing')
89
+
90
+ while file_system_updated?(@end_snapshot || current_snapshot)
91
+ finalizing
74
92
  trigger_changes(on_update)
75
93
  end
76
- @end_snapshot = nil
77
- end
78
94
 
79
- def last_found_filenames
80
- last_snapshot.keys
95
+ @end_snapshot = nil
81
96
  end
82
97
 
83
98
  private
84
99
 
85
- def last_snapshot
86
- @last_snapshot ||= mtime_snapshot
100
+ def debug(data)
101
+ @logger.debug "Thread ##{Thread.current.object_id} #{data}"
87
102
  end
88
103
 
89
- # Takes a snapshot of the current status of watched files.
90
- # (Allows avoidance of potential race condition during #finalize)
91
- def mtime_snapshot
92
- snapshot = {}
93
- filenames = expand_directories(@unexpanded_filenames)
94
-
95
- # Remove files in the exclude filenames list
96
- filenames -= expand_directories(@unexpanded_excluded_filenames)
97
-
98
- filenames.each do |filename|
99
- mtime = File.exist?(filename) ? File.mtime(filename) : Time.new(0)
100
- snapshot[filename] = mtime
101
- end
102
- snapshot
104
+ def after_initialize(*)
105
+ super if defined?(super)
103
106
  end
104
107
 
105
- def filesystem_updated?(snapshot = mtime_snapshot)
106
- @changes = {}
107
-
108
- # rubocop:disable Perfomance/HashEachMethods
109
- ## https://github.com/bbatsov/rubocop/issues/4732
110
- (snapshot.to_a - last_snapshot.to_a).each do |file, _mtime|
111
- @changes[file] = last_snapshot[file] ? :updated : :created
112
- end
108
+ def before_pause_sleep
109
+ super if defined?(super)
110
+ end
113
111
 
114
- (last_snapshot.keys - snapshot.keys).each do |file|
115
- @changes[file] = :deleted
116
- end
112
+ def before_resume_sleep
113
+ super if defined?(super)
114
+ end
117
115
 
118
- @last_snapshot = snapshot
119
- @changes.any?
116
+ def after_stop
117
+ super if defined?(super)
120
118
  end
121
119
 
122
- def expand_directories(patterns)
123
- patterns = Array(patterns) unless patterns.is_a? Array
124
- expanded_patterns = patterns.map do |pattern|
125
- pattern = File.expand_path(pattern)
126
- Dir[
127
- File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
128
- ]
129
- end
130
- expanded_patterns.flatten!
131
- expanded_patterns.uniq!
132
- expanded_patterns
120
+ def finalizing
121
+ super if defined?(super)
133
122
  end
134
123
  end
135
124
 
@@ -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,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,57 @@
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
+ # Takes a snapshot of the current status of watched files.
26
+ # (Allows avoidance of potential race condition during #finalize)
27
+ def current_snapshot
28
+ Filewatcher::Snapshot.new(
29
+ expand_directories(@unexpanded_filenames) -
30
+ expand_directories(@unexpanded_excluded_filenames)
31
+ )
32
+ end
33
+
34
+ def expand_directories(patterns)
35
+ patterns = Array(patterns) unless patterns.is_a? Array
36
+ expanded_patterns = patterns.map do |pattern|
37
+ pattern = File.expand_path(pattern)
38
+ Dir[
39
+ File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
40
+ ]
41
+ end
42
+ expanded_patterns.flatten!
43
+ expanded_patterns.uniq!
44
+ expanded_patterns
45
+ end
46
+
47
+ def file_system_updated?(snapshot = current_snapshot)
48
+ debug __method__
49
+
50
+ @changes = snapshot - @last_snapshot
51
+
52
+ @last_snapshot = snapshot
53
+
54
+ @changes.any?
55
+ end
56
+ end
57
+ 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
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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ class Filewatcher
6
+ module SpecHelper
7
+ ## Base class for Filewatcher runners in specs
8
+ class WatchRun
9
+ extend Forwardable
10
+
11
+ TMP_DIR = "#{Dir.getwd}/spec/tmp"
12
+
13
+ def_delegators Filewatcher::SpecHelper, :debug, :wait
14
+
15
+ attr_reader :filename
16
+
17
+ def initialize(filename:, action:, directory:)
18
+ @filename =
19
+ if filename.match? %r{^(/|~|[A-Z]:)} then filename
20
+ else File.join(TMP_DIR, filename)
21
+ end
22
+ @directory = directory
23
+ @action = action
24
+ debug "action = #{action}"
25
+ end
26
+
27
+ def start
28
+ debug 'start'
29
+ File.write(@filename, 'content1') unless @action == :create
30
+
31
+ wait seconds: 1
32
+ end
33
+
34
+ def run(make_changes_times: 1)
35
+ start
36
+
37
+ make_changes_times.times do
38
+ make_changes
39
+
40
+ wait seconds: 2
41
+ end
42
+
43
+ stop
44
+ end
45
+
46
+ def stop
47
+ debug 'stop'
48
+ FileUtils.rm_r(@filename) if File.exist?(@filename)
49
+ end
50
+
51
+ private
52
+
53
+ def make_changes
54
+ debug "make changes, @action = #{@action}, @filename = #{@filename}"
55
+
56
+ if @action == :delete
57
+ FileUtils.remove(@filename)
58
+ elsif @directory
59
+ FileUtils.mkdir_p(@filename)
60
+ else
61
+ ## There is no `File.write` because of strange difference in parallel `File.mtime`
62
+ ## https://cirrus-ci.com/task/6107605053472768?command=test#L497-L511
63
+ system "echo 'content2' > #{@filename}"
64
+ debug_file_mtime
65
+ end
66
+
67
+ wait seconds: 1
68
+ end
69
+
70
+ def debug_file_mtime
71
+ debug "stat #{@filename}: #{Filewatcher.system_stat(@filename)}"
72
+ debug "File.mtime = #{File.mtime(@filename).strftime('%F %T.%9N')}"
73
+ end
74
+ end
75
+ end
76
+ end