filewatcher 0.5.4 → 2.0.0.beta1

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: 1530083e28860bdfe9ffd4cc1c754b6026e25814
4
- data.tar.gz: d9fad06f13778bb85fbc6206e9975885cb83a874
2
+ SHA256:
3
+ metadata.gz: 8bff81f17e2979e1f38b9a19ed820827b4f7856b018adab83f212cb7a9777011
4
+ data.tar.gz: 6ce4ec41d01b08e4be1839d7f4fc834a66403517b0430a458ccff814ef61fc23
5
5
  SHA512:
6
- metadata.gz: 3f3044ce20143591b8c257ab09eab7c0a14b1058784020abea5653fb114ef61fa4f98e6813f61a1d7c825a54a3787b9d20416cd1086a9179524068550fbfcefa
7
- data.tar.gz: c1409c73a378c5c0debc73725904a5f8260de6e828e5e93863f8f8018eeafbb907e3aa2d735fdc423818c80aec8a8207bd76857f088106d74162d9f877bb86eb
6
+ metadata.gz: 3d58c98c188d13e760d644964d25c21edbe8d863646825fe703f08e63b7ffd58b1a702fd284f7d66aa785a939c626a5d79ccc5aa4e6abd15b2e014490d9d467f
7
+ data.tar.gz: 6a66f86cc5a4357ddf0182fdf15088f6f1935430643c47e1caef509c13795cdb181d8433b213dcd76090188ca1782f5d94557c4ff59c9ed9d27a8584ca6ff6cd
@@ -1,189 +1,130 @@
1
- # coding: utf-8
2
- # Simple file watcher. Detect changes in files and directories.
3
- #
4
- # Issues: Currently doesn't monitor changes in directorynames
5
- class FileWatcher
1
+ # frozen_string_literal: true
6
2
 
7
- attr_accessor :filenames
3
+ require 'logger'
4
+ require_relative 'filewatcher/cycles'
5
+ require_relative 'filewatcher/snapshots'
8
6
 
9
- def self.VERSION
10
- return '0.5.4'
11
- end
7
+ # Simple file watcher. Detect changes in files and directories.
8
+ #
9
+ # Issues: Currently doesn't monitor changes in directory names
10
+ class Filewatcher
11
+ include Filewatcher::Cycles
12
+ include Filewatcher::Snapshots
12
13
 
13
- def update_spinner(label)
14
- return nil unless @show_spinner
15
- @spinner ||= %w(\\ | / -)
16
- print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
17
- end
14
+ attr_accessor :interval
15
+ attr_reader :keep_watching
18
16
 
19
- def initialize(unexpanded_filenames, *args)
20
- if(args.first)
21
- options = args.first
22
- else
23
- options = {}
24
- end
17
+ def initialize(unexpanded_filenames, options = {})
25
18
  @unexpanded_filenames = unexpanded_filenames
26
19
  @unexpanded_excluded_filenames = options[:exclude]
27
- @filenames = nil
28
- @stored_update = nil
29
20
  @keep_watching = false
30
21
  @pausing = false
31
- @last_snapshot = mtime_snapshot
32
- @end_snapshot = nil
33
- @dontwait = options[:dontwait]
34
- @show_spinner = options[:spinner]
35
- @interval = options[:interval]
22
+ @immediate = options[:immediate]
23
+ @interval = options.fetch(:interval, 0.5)
24
+ @logger = options.fetch(:logger, Logger.new($stdout, level: :info))
25
+
26
+ after_initialize unexpanded_filenames, options
36
27
  end
37
28
 
38
- def watch(sleep=0.5, &on_update)
39
- trap("SIGINT") {return }
40
- @sleep = sleep
41
- if(@interval and @interval > 0)
42
- @sleep = @interval
29
+ def watch(&on_update)
30
+ ## The set of available signals depends on the OS
31
+ ## Windows doesn't support `HUP` signal, for example
32
+ (%w[HUP INT TERM] & Signal.list.keys).each do |signal|
33
+ trap(signal) { exit }
43
34
  end
44
- @stored_update = on_update
35
+
36
+ @on_update = on_update
45
37
  @keep_watching = true
46
- if(@dontwait)
47
- yield '',''
48
- end
49
- while @keep_watching
50
- @end_snapshot = mtime_snapshot if @pausing
51
- while @keep_watching && @pausing
52
- update_spinner('Pausing')
53
- Kernel.sleep @sleep
54
- end
55
- while @keep_watching && !filesystem_updated? && !@pausing
56
- update_spinner('Watching')
57
- Kernel.sleep @sleep
58
- end
59
- # test and null @updated_file to prevent yielding the last
60
- # file twice if @keep_watching has just been set to false
61
- yield @updated_file, @event if @updated_file
62
- @updated_file = nil
63
- end
64
- @end_snapshot = mtime_snapshot
38
+ yield({ '' => '' }) if @immediate
39
+
40
+ main_cycle
41
+
42
+ @end_snapshot = current_snapshot
65
43
  finalize(&on_update)
66
44
  end
67
45
 
68
46
  def pause
69
47
  @pausing = true
70
- update_spinner('Initiating pause')
71
- Kernel.sleep @sleep # Ensure we wait long enough to enter pause loop
72
- # in #watch
48
+
49
+ before_pause_sleep
50
+
51
+ # Ensure we wait long enough to enter pause loop in #watch
52
+ sleep @interval
73
53
  end
74
54
 
75
55
  def resume
76
- if !@keep_watching || !@pausing
77
- raise "Can't resume unless #watch and #pause were first called"
78
- end
79
- @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
80
59
  @pausing = false
81
- update_spinner('Resuming')
82
- Kernel.sleep @sleep # Wait long enough to exit pause loop in #watch
60
+
61
+ before_resume_sleep
62
+
63
+ sleep @interval # Wait long enough to exit pause loop in #watch
83
64
  end
84
65
 
85
66
  # Ends the watch, allowing any remaining changes to be finalized.
86
67
  # Used mainly in multi-threaded situations.
87
68
  def stop
88
69
  @keep_watching = false
89
- update_spinner('Stopping')
90
- return nil
70
+
71
+ after_stop
72
+
73
+ nil
91
74
  end
92
75
 
93
76
  # Calls the update block repeatedly until all changes in the
94
77
  # current snapshot are dealt with
95
78
  def finalize(&on_update)
96
- on_update = @stored_update if !block_given?
97
- snapshot = @end_snapshot ? @end_snapshot : mtime_snapshot
98
- while filesystem_updated?(snapshot)
99
- update_spinner('Finalizing')
100
- on_update.call(@updated_file, @event)
101
- end
102
- @end_snapshot =nil
103
- return nil
104
- end
79
+ on_update = @on_update unless block_given?
105
80
 
106
- # Takes a snapshot of the current status of watched files.
107
- # (Allows avoidance of potential race condition during #finalize)
108
- def mtime_snapshot
109
- snapshot = {}
110
- @filenames = expand_directories(@unexpanded_filenames)
111
-
112
- if(@unexpanded_excluded_filenames != nil and @unexpanded_excluded_filenames.size > 0)
113
- # Remove files in the exclude filenames list
114
- @filtered_filenames = []
115
- @excluded_filenames = expand_directories(@unexpanded_excluded_filenames)
116
- @filenames.each do |filename|
117
- if(not(@excluded_filenames.include?(filename)))
118
- @filtered_filenames << filename
119
- end
120
- end
121
- @filenames = @filtered_filenames
81
+ while file_system_updated?(@end_snapshot || current_snapshot)
82
+ finalizing
83
+ trigger_changes(on_update)
122
84
  end
123
85
 
124
- @filenames.each do |filename|
125
- mtime = File.exist?(filename) ? File.stat(filename).mtime : Time.new(0)
126
- snapshot[filename] = mtime
127
- end
128
- return snapshot
86
+ @end_snapshot = nil
129
87
  end
130
88
 
131
- def filesystem_updated?(snapshot_to_use = nil)
132
- snapshot = snapshot_to_use ? snapshot_to_use : mtime_snapshot
133
- forward_changes = snapshot.to_a - @last_snapshot.to_a
134
-
135
- forward_changes.each do |file,mtime|
136
- @updated_file = file
137
- unless @last_snapshot.fetch(@updated_file,false)
138
- @last_snapshot[file] = mtime
139
- @event = :new
140
- return true
141
- else
142
- @last_snapshot[file] = mtime
143
- @event = :changed
144
- return true
145
- end
146
- end
89
+ private
147
90
 
148
- backward_changes = @last_snapshot.to_a - snapshot.to_a
149
- forward_names = forward_changes.map{|change| change.first}
150
- backward_changes.reject!{|f,m| forward_names.include?(f)}
151
- backward_changes.each do |file,mtime|
152
- @updated_file = file
153
- @last_snapshot.delete(file)
154
- @event = :delete
155
- return true
91
+ def expand_directories(patterns)
92
+ patterns = Array(patterns) unless patterns.is_a? Array
93
+ expanded_patterns = patterns.map do |pattern|
94
+ pattern = File.expand_path(pattern)
95
+ Dir[
96
+ File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
97
+ ]
156
98
  end
157
- return false
99
+ expanded_patterns.flatten!
100
+ expanded_patterns.uniq!
101
+ expanded_patterns
158
102
  end
159
103
 
160
- def last_found_filenames
161
- @last_snapshot.keys
104
+ def debug(data)
105
+ @logger.debug "Thread ##{Thread.current.object_id} #{data}"
162
106
  end
163
107
 
164
- def expand_directories(patterns)
165
- if(!patterns.kind_of?Array)
166
- patterns = [patterns]
167
- end
168
- patterns.map { |it| Dir[fulldepth(expand_path(it))] }.flatten.uniq
108
+ def after_initialize(*)
109
+ super if defined?(super)
169
110
  end
170
111
 
171
- private
112
+ def before_pause_sleep
113
+ super if defined?(super)
114
+ end
172
115
 
173
- def fulldepth(pattern)
174
- if File.directory? pattern
175
- "#{pattern}/**/*"
176
- else
177
- pattern
178
- end
116
+ def before_resume_sleep
117
+ super if defined?(super)
179
118
  end
180
119
 
181
- def expand_path(pattern)
182
- if pattern.start_with?('~')
183
- File.expand_path(pattern)
184
- else
185
- pattern
186
- end
120
+ def after_stop
121
+ super if defined?(super)
187
122
  end
188
123
 
124
+ def finalizing
125
+ super if defined?(super)
126
+ end
189
127
  end
128
+
129
+ # Require at end of file to not overwrite `Filewatcher` class
130
+ require_relative 'filewatcher/version'
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Filewatcher
4
+ # Module for all cycles in `Filewatcher#watch`
5
+ module Cycles
6
+ private
7
+
8
+ def main_cycle
9
+ while @keep_watching
10
+ @end_snapshot = current_snapshot if @pausing
11
+
12
+ pausing_cycle
13
+
14
+ watching_cycle
15
+
16
+ # test and clear @changes to prevent yielding the last
17
+ # changes twice if @keep_watching has just been set to false
18
+ trigger_changes
19
+ end
20
+ end
21
+
22
+ def pausing_cycle
23
+ while @keep_watching && @pausing
24
+ before_pausing_sleep
25
+
26
+ sleep @interval
27
+ end
28
+ end
29
+
30
+ def before_pausing_sleep
31
+ super if defined?(super)
32
+ end
33
+
34
+ def watching_cycle
35
+ @last_snapshot ||= current_snapshot
36
+ loop do
37
+ before_watching_sleep
38
+
39
+ debug "#{__method__} sleep #{@interval}"
40
+ sleep @interval
41
+ break if !@keep_watching || file_system_updated? || @pausing
42
+ end
43
+ end
44
+
45
+ def before_watching_sleep
46
+ super if defined?(super)
47
+ end
48
+
49
+ def trigger_changes(on_update = @on_update)
50
+ debug __method__
51
+ on_update.call(@changes.dup) unless @changes.empty?
52
+ @changes.clear
53
+ debug '@changes cleared'
54
+ end
55
+ end
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