filewatcher 1.1.0 → 2.0.0.beta4

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: ed73f2615aeb5418438bb443abc274bc2cd8279f
4
- data.tar.gz: 716e8b582f81531d486c1a281149da1702a044b7
2
+ SHA256:
3
+ metadata.gz: 5e9ecdeee0c20df3055e1078a67e8194040361fbcea9f1c633f2b33a8177f250
4
+ data.tar.gz: ba5abe65b765b0cc4c4630aae0cf1e0751c7ea4dec5f39911a9de9510fff7e9e
5
5
  SHA512:
6
- metadata.gz: b37524eaae72f8eaaad40766e4e1793ec5766ebe7dd454e310eb98ba54745c59ecbc8358ea2045e335d242c84d6e48ef5bc1d0718b75546af481bd221f25c5d9
7
- data.tar.gz: 50690825a35a747dce1491d7f3f77a2edf135797551ca625cdeeafb08efd31caada6e982de54fde6bdad88d4ae934b92bb66d29605a7d74e181bc5d059f9d367
6
+ metadata.gz: 287d42ea2fb239be62a4a0701f5b84440c409b6dc93c3b8caf5a5c8c87958ab12f4885e146e679f988dc2d2ef9c7d47d77be79ce4b8dd6d0ea97d71d63de9116
7
+ data.tar.gz: 2a296164258b091771802bbeb7d33690160e235220d30644743abe441d7801259dcad5bcd4216655c1e57549b9052edb45f1b1440a23ba1d84e4981f278a4e90
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,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
- @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)
@@ -37,28 +44,31 @@ class Filewatcher
37
44
 
38
45
  @on_update = on_update
39
46
  @keep_watching = true
40
- yield('', '') if @immediate
47
+ yield({ '' => '' }) if @immediate
41
48
 
42
49
  main_cycle
43
50
 
44
- @end_snapshot = mtime_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
- update_spinner('Initiating pause')
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
- raise "Can't resume unless #watch and #pause were first called"
58
- end
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
- update_spinner('Resuming')
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
- update_spinner('Stopping')
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
- while filesystem_updated?(@end_snapshot || mtime_snapshot)
78
- update_spinner('Finalizing')
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
- def last_found_filenames
85
- last_snapshot.keys
95
+ @end_snapshot = nil
86
96
  end
87
97
 
88
98
  private
89
99
 
90
- def last_snapshot
91
- @last_snapshot ||= mtime_snapshot
100
+ def debug(data)
101
+ @logger.debug "Thread ##{Thread.current.object_id} #{data}"
92
102
  end
93
103
 
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
104
+ def after_initialize(*)
105
+ super if defined?(super)
108
106
  end
109
107
 
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
108
+ def before_pause_sleep
109
+ super if defined?(super)
110
+ end
116
111
 
117
- (last_snapshot.keys - snapshot.keys).each do |file|
118
- @changes[file] = :deleted
119
- end
112
+ def before_resume_sleep
113
+ super if defined?(super)
114
+ end
120
115
 
121
- @last_snapshot = snapshot
122
- @changes.any?
116
+ def after_stop
117
+ super if defined?(super)
123
118
  end
124
119
 
125
- def expand_directories(patterns)
126
- patterns = Array(patterns) unless patterns.is_a? Array
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
 
@@ -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,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,84 @@
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
+ module_function
17
+
18
+ def logger
19
+ @logger ||= Logger.new($stdout, level: :debug)
20
+ end
21
+
22
+ def environment_specs_coefficients
23
+ @environment_specs_coefficients ||= {
24
+ -> { ENV['CI'] } => 1,
25
+ -> { RUBY_PLATFORM == 'java' } => 1,
26
+ -> { Gem::Platform.local.os == 'darwin' } => 1
27
+ }
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'
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
+ end
84
+ 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, :system_stat
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}: #{system_stat(@filename)}"
72
+ debug "File.mtime = #{File.mtime(@filename).strftime('%F %T.%9N')}"
73
+ end
74
+ end
75
+ end
76
+ end