filewatcher 1.0.1 → 2.0.0.beta3

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
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