filewatcher 0.4.0 → 0.5.0

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
  SHA1:
3
- metadata.gz: 2fb39cdddc7a64525396dac78226807bb53e450a
4
- data.tar.gz: 641cea3ede6d8d1eb20fe7a0b8aa5ad75e86d75e
3
+ metadata.gz: 43546a6002f63f8251ab718f13676e812c47a64c
4
+ data.tar.gz: 734a6f6708d85692d9141644a1f583f5633c1dc3
5
5
  SHA512:
6
- metadata.gz: 728a408d87cfb6be8eb5556f31bc74972645275d0bb0c3c9a1045a9d4287e1aa3ff4f5a15fb46dfa14953e837ed2b38cdcfc8ad8e34e380b6f2980a9cebbc932
7
- data.tar.gz: bad167aae4d3e3e7fe9848ea7295f1a0fd71e4984480f35765ee869814eeee8555f0fe705b5b61a33c7dc4cc8fd95d3e6bbce6410935e3c0c22b3233effcc950
6
+ metadata.gz: 240dd96db19430b21f3fd29baedd6d3ad4a7cd3fb1089e76930de90a6d38f5fbc2c6f1d55fa34c6a17ae1192352021cc82395e14344666ff0dc8649ff7e82951
7
+ data.tar.gz: ef86c4909413021b72f7c93e7a46451f62bc358a8db175c0757c837627ef0d37dd3fabd1441f2fbeced9cd728148527aa8fa2743f5e026d427b5010af3751359
data/README.md CHANGED
@@ -84,12 +84,16 @@ filesystem gets updated:
84
84
 
85
85
  $ filewatcher "src test" "ruby test/test_suite.rb"
86
86
 
87
- Automatic restart of processes
88
- ------------------------------
87
+ Restart long running commands
88
+ -----------------------------
89
+
90
+ The `--restart` option kills the command if it's still running when a filesystem change happens. Can be used to restart locally running webservers on updates, or kill long running tests and restart on updates. This option often makes filewatcher faster in general. To not wait for tests to finish:
89
91
 
90
- The `--restart` option restarts the command when changes happens on the file system. The `--dontwait` starts the command when filewatcher is started. To start a webserver and have it automatically restart when html files are updated:
92
+ $ filewatcher --restart "**/*.rb" "rake test"
91
93
 
92
- $ filewatcher --dontwait --restart "*.html" "python -m SimpleHTTPServer"
94
+ The `--dontwait` option starts the command on startup without waiting for filesystem updates. To start a webserver and have it automatically restart when html files are updated:
95
+
96
+ $ filewatcher --restart --dontwait "*.html" "python -m SimpleHTTPServer"
93
97
 
94
98
  Available enviroment variables
95
99
  ------------------------------
@@ -105,13 +109,20 @@ The environment variables $FILEPATH, $FILEDIR and $FSEVENT is also available.
105
109
  Command line options
106
110
  --------------------
107
111
 
112
+ Useful command line options:
113
+
114
+ --list, -l: Print name of files being watched on startup
115
+ --restart, -r: Kill the command if it's still running
116
+ --dontwait, -d: Start the command immediately
117
+
118
+ Other command line options:
119
+
120
+ --version, -v: Print version and exit
121
+ --help, -h: Show this message
108
122
  --interval, -i <f>: Interval in seconds to scan filesystem. Defaults to 0.5 seconds.
109
- --exec, -e: Execute file as a script when file is updated.
123
+ --exec, -e: Execute file as a script when file is updated
110
124
  --include, -n <s>: Include files (default: *)
111
125
  --exclude, -x <s>: Exclude file(s) matching (default: "")
112
- --list, -l: Print name of files being watched
113
- --version, -v: Print version and exit
114
- --help, -h: Show this message
115
126
 
116
127
  Ruby API
117
128
  --------
@@ -188,18 +199,41 @@ for syntax.
188
199
  puts "Updated " + filename
189
200
  end
190
201
 
191
- The filewatcher library is just a single file with 96 LOC (including comments)
202
+ Start, pause, resume, stop, and finalize a running watch. This is particularly
203
+ useful when the update block takes a while to process each file (eg. sending
204
+ over the network)
205
+
206
+ filewatcher = FileWatcher.new(["*.rb"])
207
+ thread = Thread.new(filewatcher){|fw| fw.watch{|f| puts "Updated " + f}}
208
+ ...
209
+ filewatcher.pause # block stops responding to filesystem changes
210
+ filewatcher.finalize # Ensure all filesystem changes made prior to
211
+ # pausing are handled.
212
+ ...
213
+ filewatcher.resume # block begins responding again, but is not given
214
+ # changes made between #pause_watch and
215
+ # #resume_watch
216
+ ...
217
+ filewatcher.end # block stops responding to filesystem changes
218
+ # and takes a final snapshot of the filesystem
219
+ thread.join
220
+
221
+ filewatcher.finalize # Ensure all filesystem changes made prior to
222
+ # ending the watch are handled.
223
+
224
+ The filewatcher library is just a single file with 147 LOC (including comments)
192
225
  with no dependencies.
193
226
 
194
227
 
195
228
  Credits
196
229
  -------
197
- This project would not be where it is today without the generous help provided by people reporting issues and these contributors:
198
-
230
+ This project would not be where it is today without the generous help provided by people reporting issues and these contributors:
199
231
 
200
- * Support for absolute and globbed paths by Franco Leonardo Bulgarelli: https://github.com/flbulgarelli
232
+ * Penn Taylor: Spinner displayed in the terminal and Start, pause, resume, stop, and finalize a running watch.
233
+
234
+ * Franco Leonardo Bulgarelli: Support for absolute and globbed paths by https://github.com/flbulgarelli
201
235
 
202
- * Command line globbing by Kristoffer Roupé https://github.com/kitofr
236
+ * Kristoffer Roupé https://github.com/kitofr Command line globbing by
203
237
 
204
238
  This gem was initially inspired by Tom Lieber's blogg posting: http://alltom.com/pages/detecting-file-changes-with-ruby
205
239
 
data/bin/filewatcher CHANGED
@@ -29,10 +29,11 @@ EOS
29
29
  opt :exec, "Execute file as a script when file is updated.", :short => 'e', :type => :boolean, :default => false
30
30
  opt :include, "Include files", :type => :string, :default => "*"
31
31
  opt :exclude, "Exclude file(s) matching", :type => :string, :default => ""
32
- opt :interval, "Interval to scan filesystem. Defaults to 0.5 seconds.", :short => 'i', :type => :float, :default => 0.5
32
+ opt :interval, "Interval to scan filesystem.", :short => 'i', :type => :float, :default => 0.5
33
+ opt :quiet, "Hide spinner", :short => 'q', :type => :boolean, :default => false
33
34
  end
34
35
 
35
- Trollop::die "must have at least one argument" if(ARGV.size == 0)
36
+ Trollop::die Trollop::educate if(ARGV.size == 0)
36
37
 
37
38
  files = []
38
39
  ARGV[0...-1].each do |a|
@@ -59,37 +60,13 @@ end
59
60
 
60
61
  files = split_files_void_escaped_whitespace(files)
61
62
 
62
- def fork_process(env, cmd)
63
- pipe_read, pipe_write = IO.pipe
64
- fork do
65
- pipe_read.close
66
- pipe_write.write Process.spawn(env,cmd).to_s
67
- exit
68
- end
69
-
70
- Kernel.sleep 1
71
- pipe_write.close
72
- child_pid = pipe_read.read.to_i
73
- pipe_read.close
74
- return child_pid
75
- end
76
-
77
- def kill_process(child_pid)
78
- still_running = true
79
- begin
80
- Process.kill(1, child_pid)
81
- rescue Errno::ESRCH
82
- still_running = false
83
- end
84
- while still_running do
85
- begin
86
- Process.getpgid( child_pid)
87
- still_running = true
88
- rescue Errno::ESRCH
89
- still_running = false
90
- end
91
- Kernel.sleep 0.1
92
- end
63
+ def restart(child_pid, env, cmd)
64
+ Process.kill(9,child_pid)
65
+ Process.wait(child_pid)
66
+ rescue Errno::ESRCH
67
+ # already killed
68
+ ensure
69
+ child_pid = Process.spawn(env, cmd)
93
70
  end
94
71
 
95
72
  if(options[:restart])
@@ -97,70 +74,74 @@ if(options[:restart])
97
74
  child_pid = nil
98
75
  end
99
76
 
100
- FileWatcher.new(files, options[:list], options[:dontwait]).watch(options[:interval]) do |filename, event|
101
- cmd = nil
102
- if(options[:exec] and File.exist?(filename))
103
- extension = filename[/(\.[^\.]*)$/,0]
104
- runners = {
105
- ".py" => "python",
106
- ".js" => "node",
107
- ".rb" => "ruby",
108
- ".pl" => "perl",
109
- ".awk" => "awk",
110
- ".php" => "php",
111
- ".phtml" => "php",
112
- ".php4" => "php",
113
- ".php3" => "php",
114
- ".php5" => "php",
115
- ".phps" => "php"
116
- }
117
- runner = runners[extension]
118
- if(runner)
119
- cmd = "env #{runner.to_s} #{filename}"
77
+ begin
78
+ fw = FileWatcher.new(files, options[:list], options[:dontwait], !options[:quiet])
79
+ fw.watch(options[:interval]) do |filename, event|
80
+ cmd = nil
81
+ if(options[:exec] and File.exist?(filename))
82
+ extension = filename[/(\.[^\.]*)$/,0]
83
+ runners = {
84
+ ".py" => "python",
85
+ ".js" => "node",
86
+ ".rb" => "ruby",
87
+ ".pl" => "perl",
88
+ ".awk" => "awk",
89
+ ".php" => "php",
90
+ ".phtml" => "php",
91
+ ".php4" => "php",
92
+ ".php3" => "php",
93
+ ".php5" => "php",
94
+ ".phps" => "php"
95
+ }
96
+ runner = runners[extension]
97
+ if(runner)
98
+ cmd = "env #{runner.to_s} #{filename}"
99
+ end
100
+ elsif(ARGV.length > 1)
101
+ cmd = ARGV[-1]
120
102
  end
121
- elsif(ARGV.length > 1)
122
- cmd = ARGV[-1]
123
- end
124
103
 
125
- if(cmd)
126
- path = Pathname.new(filename)
127
- env = {
128
- 'FILENAME'=> path.to_s,
129
- 'FILEDIR' => path.parent.realpath.to_s,
130
- 'FSEVENT' => event.to_s
131
- }
132
- if(event != :delete)
133
- ENV['FILEPATH'] = path.realpath.to_s
134
- end
104
+ if(cmd)
105
+ path = Pathname.new(filename)
106
+ env = {
107
+ 'FILENAME'=> path.to_s,
108
+ 'FILEDIR' => path.parent.realpath.to_s,
109
+ 'FSEVENT' => event.to_s
110
+ }
111
+ if(event != :delete)
112
+ ENV['FILEPATH'] = path.realpath.to_s
113
+ end
135
114
 
136
- if(options[:restart])
137
- if(child_pid == nil)
138
- child_pid = fork_process(env, cmd)
115
+ if(options[:restart])
116
+ if(child_pid == nil)
117
+ child_pid = Process.spawn(env, cmd)
118
+ else
119
+ child_id = restart(child_pid, env, cmd)
120
+ end
139
121
  else
140
- kill_process(child_pid)
141
- child_pid = fork_process(env, cmd)
122
+ begin
123
+ pid = Process.spawn(env, cmd)
124
+ Process.wait()
125
+ rescue SystemExit, Interrupt
126
+ exit(0)
127
+ end
142
128
  end
129
+
143
130
  else
144
- begin
145
- pid = Process.spawn(env, cmd)
146
- Process.wait()
147
- rescue SystemExit, Interrupt
148
- exit(0)
131
+ case(event)
132
+ when :changed
133
+ print "file updated"
134
+ when :delete
135
+ print "file deleted"
136
+ when :new
137
+ print "new file"
138
+ else
139
+ print event.to_s
149
140
  end
141
+ puts ": " + filename
150
142
  end
151
143
 
152
- else
153
- case(event)
154
- when :changed
155
- print "file updated"
156
- when :delete
157
- print "file deleted"
158
- when :new
159
- print "new file"
160
- else
161
- print event.to_s
162
- end
163
- puts ": " + filename
164
144
  end
165
-
145
+ rescue SystemExit, Interrupt
146
+ fw.finalize
166
147
  end
data/lib/filewatcher.rb CHANGED
@@ -6,76 +6,135 @@ class FileWatcher
6
6
  attr_accessor :filenames
7
7
 
8
8
  def self.VERSION
9
- return '0.4.0'
9
+ return '0.5.0'
10
10
  end
11
11
 
12
- def initialize(unexpanded_filenames, print_filelist=false, dontwait=false)
12
+ def update_spinner(label)
13
+ return nil unless @show_spinner
14
+ @spinner ||= %w(\\ | / -)
15
+ print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
16
+ end
17
+
18
+ def initialize(unexpanded_filenames, print_filelist=false, dontwait=false, show_spinner=false)
13
19
  @unexpanded_filenames = unexpanded_filenames
14
- @last_mtimes = { }
15
- @filenames = expand_directories(@unexpanded_filenames)
20
+ @filenames = nil
21
+ @stored_update = nil
22
+ @keep_watching = false
23
+ @pausing = false
24
+ @last_snapshot = mtime_snapshot
25
+ @end_snapshot = nil
16
26
  @dontwait = dontwait
27
+ @show_spinner = show_spinner
17
28
  puts 'Watching:' if print_filelist
18
29
  @filenames.each do |filename|
19
30
  raise 'File does not exist' unless File.exist?(filename)
20
- @last_mtimes[filename] = File.stat(filename).mtime
21
31
  puts filename if print_filelist
22
32
  end
23
33
  end
24
34
 
25
- def watch(sleep=1, &on_update)
35
+ def watch(sleep=0.5, &on_update)
36
+ @sleep = sleep
37
+ @stored_update = on_update
38
+ @keep_watching = true
26
39
  if(@dontwait)
27
40
  yield '',''
28
41
  end
29
- loop do
30
- begin
31
- Kernel.sleep sleep until filesystem_updated?
32
- rescue SystemExit,Interrupt
33
- Kernel.exit
42
+ while @keep_watching
43
+ @end_snapshot = mtime_snapshot if @pausing
44
+ while @keep_watching && @pausing
45
+ update_spinner('Pausing')
46
+ Kernel.sleep sleep
34
47
  end
35
- yield @updated_file, @event
48
+ while @keep_watching && !filesystem_updated? && !@pausing
49
+ update_spinner('Scanning')
50
+ Kernel.sleep sleep
51
+ end
52
+ # test and null @updated_file to prevent yielding the last
53
+ # file twice if @keep_watching has just been set to false
54
+ yield @updated_file, @event if @updated_file
55
+ @updated_file = nil
36
56
  end
57
+ @end_snapshot = mtime_snapshot
58
+ finalize(&on_update)
37
59
  end
38
60
 
39
- def filesystem_updated?
40
- filenames = expand_directories(@unexpanded_filenames)
61
+ def pause
62
+ @pausing = true
63
+ update_spinner('Initiating pause')
64
+ Kernel.sleep @sleep # Ensure we wait long enough to enter pause loop
65
+ # in #watch
66
+ end
41
67
 
42
- if(filenames.size > @filenames.size)
43
- filename = (filenames - @filenames).first
44
- @filenames << filename
45
- @last_mtimes[filename] = File.stat(filename).mtime
46
- @updated_file = filename
47
- @event = :new
48
- return true
68
+ def resume
69
+ if !@keep_watching || !@pausing
70
+ raise "Can't resume unless #watch and #pause were first called"
49
71
  end
72
+ @last_snapshot = mtime_snapshot # resume with fresh snapshot
73
+ @pausing = false
74
+ update_spinner('Resuming')
75
+ Kernel.sleep @sleep # Wait long enough to exit pause loop in #watch
76
+ end
50
77
 
51
- if(filenames.size < @filenames.size)
52
- filename = (@filenames - filenames).first
53
- @filenames.delete(filename)
54
- @last_mtimes.delete(filename)
55
- @updated_file = filename
56
- @event = :delete
57
- return true
78
+ # Ends the watch, allowing any remaining changes to be finalized.
79
+ # Used mainly in multi-threaded situations.
80
+ def stop
81
+ @keep_watching = false
82
+ update_spinner('Stopping')
83
+ return nil
84
+ end
85
+
86
+ # Calls the update block repeatedly until all changes in the
87
+ # current snapshot are dealt with
88
+ def finalize(&on_update)
89
+ on_update = @stored_update if !block_given?
90
+ snapshot = @end_snapshot ? @end_snapshot : mtime_snapshot
91
+ while filesystem_updated?(snapshot)
92
+ update_spinner('Finalizing')
93
+ on_update.call(@updated_file, @event)
58
94
  end
95
+ @end_snapshot =nil
96
+ return nil
97
+ end
59
98
 
99
+ # Takes a snapshot of the current status of watched files.
100
+ # (Allows avoidance of potential race condition during #finalize)
101
+ def mtime_snapshot
102
+ snapshot = {}
103
+ @filenames = expand_directories(@unexpanded_filenames)
60
104
  @filenames.each do |filename|
61
- if(not(File.exists?(filename)))
62
- @filenames.delete(filename)
63
- @last_mtimes.delete(filename)
64
- @updated_file = filename
65
- @event = :delete
66
- return true
67
- end
68
- mtime = File.stat(filename).mtime
69
- updated = @last_mtimes[filename] < mtime
105
+ mtime = File.exist?(filename) ? File.stat(filename).mtime : Time.new(0)
106
+ snapshot[filename] = mtime
107
+ end
108
+ return snapshot
109
+ end
110
+
111
+ def filesystem_updated?(snapshot_to_use = nil)
112
+ snapshot = snapshot_to_use ? snapshot_to_use : mtime_snapshot
70
113
 
71
- @last_mtimes[filename] = mtime
72
- if(updated)
73
- @updated_file = filename
114
+ forward_changes = snapshot.to_a - @last_snapshot.to_a
115
+
116
+ forward_changes.each do |file,mtime|
117
+ @updated_file = file
118
+ unless @last_snapshot.fetch(@updated_file,false)
119
+ @last_snapshot[file] = mtime
120
+ @event = :new
121
+ return true
122
+ else
123
+ @last_snapshot[file] = mtime
74
124
  @event = :changed
75
125
  return true
76
126
  end
77
127
  end
78
128
 
129
+ backward_changes = @last_snapshot.to_a - snapshot.to_a
130
+ forward_names = forward_changes.map{|change| change.first}
131
+ backward_changes.reject!{|f,m| forward_names.include?(f)}
132
+ backward_changes.each do |file,mtime|
133
+ @updated_file = file
134
+ @last_snapshot.delete(file)
135
+ @event = :delete
136
+ return true
137
+ end
79
138
  return false
80
139
  end
81
140
 
@@ -1 +1 @@
1
- content2
1
+ content1
@@ -1 +1 @@
1
- content
1
+ content2
@@ -98,4 +98,61 @@ describe FileWatcher do
98
98
  filewatcher.filesystem_updated?.should.be.true
99
99
  end
100
100
 
101
+ it "should be stoppable" do
102
+ filewatcher = FileWatcher.new(["test/fixtures"])
103
+ thread = Thread.new(filewatcher){filewatcher.watch(0.1)}
104
+ sleep 0.2 # thread needs a chance to start
105
+ filewatcher.stop
106
+ thread.join.should.equal thread # Proves thread successfully joined
107
+ end
108
+
109
+ it "should be pauseable/resumable" do
110
+ filewatcher = FileWatcher.new(["test/fixtures"])
111
+ filewatcher.filesystem_updated?.should.be.false
112
+ processed = []
113
+ thread = Thread.new(filewatcher,processed) do
114
+ filewatcher.watch(0.1){|f,e| processed << f }
115
+ end
116
+ sleep 0.2 # thread needs a chance to start
117
+ filewatcher.pause
118
+ (1..4).each do |n|
119
+ open("test/fixtures/file#{n}.txt","w") { |f| f.puts "content#{n}" }
120
+ end
121
+ sleep 0.2 # Give filewatcher time to respond
122
+ processed.should.equal [] #update block should not have been called
123
+ filewatcher.resume
124
+ sleep 0.2 # Give filewatcher time to respond
125
+ processed.should.equal [] #update block still should not have been called
126
+ added_files = []
127
+ (5..7).each do |n|
128
+ added_files << "test/fixtures/file#{n}.txt"
129
+ open(added_files.last,"w") { |f| f.puts "content#{n}" }
130
+ end
131
+ sleep 0.2 # Give filewatcher time to respond
132
+ filewatcher.stop
133
+ processed.should.satisfy &includes_all(added_files)
134
+ end
135
+
136
+ it "should process all remaining changes at finalize" do
137
+ filewatcher = FileWatcher.new(["test/fixtures"])
138
+ filewatcher.filesystem_updated?.should.be.false
139
+ processed = []
140
+ thread = Thread.new(filewatcher,processed) do
141
+ filewatcher.watch(0.1){|f,e| processed << f }
142
+ end
143
+ sleep 0.2 # thread needs a chance to start
144
+ filewatcher.stop
145
+ thread.join
146
+ added_files = []
147
+ (1..4).each do |n|
148
+ added_files << "test/fixtures/file#{n}.txt"
149
+ open(added_files.last,"w") { |f| f.puts "content#{n}" }
150
+ end
151
+ filewatcher.finalize
152
+ puts "What is wrong with finalize:"
153
+ puts "Expect: #{added_files.inspect}"
154
+ puts "Actual: #{processed.inspect}"
155
+ processed.should.satisfy &includes_all(added_files)
156
+ end
157
+
101
158
  end
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filewatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Flemming
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-06 00:00:00.000000000 Z
11
+ date: 2015-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trollop
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '2.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10.3'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.3'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bacon
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '1.2'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.2'
55
55
  description: Detect changes in filesystem. Works anywhere.
@@ -82,12 +82,12 @@ require_paths:
82
82
  - lib
83
83
  required_ruby_version: !ruby/object:Gem::Requirement
84
84
  requirements:
85
- - - '>='
85
+ - - ">="
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  requirements:
90
- - - '>='
90
+ - - ">="
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []