Watching files for changes
Polling the timestamp
The simplest watcher is a comparison. Remember File.mtime(path), look again later, and the file changed when the two are different. The timestamp has sub-second precision on a modern file system, so two reads a tenth of a second apart on an untouched file agree exactly:
ruby -e 'File.write("a.txt", "x\n"); 2.times { puts File.mtime("a.txt").strftime("%H:%M:%S.%N"); sleep 0.1 }'
00:20:26.417165513
00:20:26.417165513
Identical lines mean no change; a different line means the contents or the metadata moved. %N prints nanoseconds, which is what makes the comparison meaningful for a quick edit.
A related question is cheaper still: was the file touched recently? mtime > Time.now - 60 answers "less than a minute ago" without any stored state:
ruby -e 'File.write("a.txt", "x\n"); p File.mtime("a.txt") > Time.now - 60'
true
A loop with a stop condition
A watcher is a loop that must end. Here it ends when the timestamp moves or when a deadline passes, and the writer is a thread that plays the part of the editor:
path = "watched.txt"
File.write(path, "one\n")
seen = File.mtime(path)
# stand-in for the process that writes the file
writer = Thread.new do
sleep 0.5
File.write(path, "two\n")
end
deadline = Time.now + 3
loop do
now = File.mtime(path)
if now != seen
puts "changed at #{now.strftime("%H:%M:%S.%N")}"
break
end
break puts "timeout" if Time.now > deadline
sleep 0.2
end
writer.join
ruby watch.rb
changed at 00:14:57.602786520
The deadline is the important line. A watcher without one runs until somebody kills it, and a test that waits for a change it never gets hangs the suite. The interval of 0.2 seconds is the other knob: a fast poll notices a change quickly and costs a stat every fifth of a second.
What polling misses
Polling compares states at intervals, so it cannot see a change that starts and ends between two looks. Three edits in one interval look like one, and an edit that restores the old mtime is invisible. On a file system with one-second timestamps, a quick rewrite in the same second may not move the value at all.
The other cost is the wait itself: with a 0.2 second interval a change is noticed up to 0.2 seconds late, and a watcher for many files does one stat per file per interval. That is fine for a configuration file and for a script that regenerates output, and it is the wrong shape for a development server that watches a whole source tree.
When a watcher gem is worth it
A real watcher asks the operating system instead of the clock: inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows. The listen gem wraps all three, and a reloader such as Guard or the Rails development server is built on it:
require "listen"
listener = Listen.to("src") do |modified, added, removed|
puts "modified: #{modified.size}"
puts "added: #{added.size}"
puts "removed: #{removed.size}"
end
listener.start
sleep
The events arrive as lists of paths, and the callback runs once per batch. The trade is a gem and a native dependency against missing an edit, and for a folder a person edits in an editor the gem wins.
listen is a gem, so the block above is a sketch rather than a command run here. Add it to the Gemfile and run the script: the library picks the backend per platform.
What to watch for
File.mtime raises
Errno::ENOENT while the file does not exist. A
watcher that also cares about creation checks
File.exist? first and treats a missing file as a
state, not an error.
An editor often saves by writing a new file and renaming it over the old
one, so the path keeps existing but the inode changes. Polling sees the
new mtime and is fine; an open handle from before the save still points
at the old contents, which is why a reloader reopens the file.
This is the last lesson of the chapter. The Ruby course index has the whole tree, and the next lesson is String tools.
Article author: Arthur Isaev