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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 805761615b226e14eb6431116a32bdc34e8b13cf
4
- data.tar.gz: 4c1523220b4c242d33750e145091e1e2568766ac
2
+ SHA256:
3
+ metadata.gz: c4552a19cac521012fb0cca2c232a14f501530262e02639ed017c0897e5f985f
4
+ data.tar.gz: f3d98c14badcbd439a9dca5ce289b40cc80aecb1c377d8ec08f80e76c398aaf5
5
5
  SHA512:
6
- metadata.gz: 6d6f196a0ce277f920732a6c4ff0071b82e5b6eb3cf0683834041e973bb3a10a6c914aba6495d0bbbdfd83d241f983f697b45aa8ddd62ad9c973e3b4294fbffa
7
- data.tar.gz: 7a75c0bb5e79efa44207f20052f8907790e2292195f65336f3d06303a8618ebe84fc658e32fec8333da461d7ccc979c74df061c735692ff7dd2647fac70b0f91
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 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,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