filewatcher 0.5.4 → 2.0.0.beta1

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