Class: FileMonitor

Inherits:
Object
  • Object
show all
Defined in:
lib/FileMonitor.rb

Overview

Purpose: Watches the file system for changes

Usage:

require 'filemonitor'

# Create a FileMonitor instance and assign the callback to the entire object.
# In the following example, "watched_item" is a MonitoredItems::Store, see the new
# method for a better example of working in your block.
file_spy = FileMonitor.new do |watched_item| ... end

# Any files in the working directory (Dir.pwd) and its sub-directories will
#  be watched for changes.  If a change is found the callback assigned above 
#  will be enacted.
file_spy << Dir.pwd
file_spy << "/path/to/other/file.rb"

# launch an independent process to do the monitoring:
file_spy.spawn

# Alternatively you can do all of the above in one line:
FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") do |watched_item| ... end

Constant Summary collapse

VERSION =
'0.0.4'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}, &callback) ⇒ FileMonitor

The new method may be called with an optional callback which must be a block either do…end or …. The block may consist of upto two arguments ie {|watched_item, monitored|}, in which case, the watched_item is an instance of FileMonitor::Store and monitored is the FileMonitor instances self.

The first argument of the block, ‘watched_item’ in this case, will respond to: (:path, :modified & :callback). The second argument of the block, ‘monitored’ in this case, will respond any FileMonitor method.

Example:

FileMonitor.new do |watched_item|
  puts "My file name & path is: #{watched_item.path}"
  puts "When I find a change I will call watched_item.callback which displays this text."
end

FileMonitor.new do |watched_item, monitored|
  # Add files from a file that is a list of files to watch... Note: There
  IO.readlines(watched_item.path).each {|file| monitored << file } if watched_item.path == '/path/to/file/watchme.list'
  # Clear watchme.list so we won't add all watch files every time the file changes
  open(watched_item) { |f| puts "This is the callback that is run..." }
end


49
50
51
52
53
54
55
# File 'lib/FileMonitor.rb', line 49

def initialize(options={}, &callback)
  @options=options
  # @options[:persistent] ||= false
  @watched = []
  @callback = callback if callback.is_a? Proc
  @options[:rescan_directories] ||= true
end

Instance Attribute Details

#callbackObject

Returns the value of attribute callback.



28
29
30
# File 'lib/FileMonitor.rb', line 28

def callback
  @callback
end

#pidObject

Returns the value of attribute pid.



28
29
30
# File 'lib/FileMonitor.rb', line 28

def pid
  @pid
end

#watchedObject

Returns the value of attribute watched.



28
29
30
# File 'lib/FileMonitor.rb', line 28

def watched
  @watched
end

Class Method Details

.when_modified(*paths, &callback) ⇒ Object

Returns a spawned FileMonitor instance. The independent process automatically calls the given callback when changes are found.

Example:

fm = FileMonitor.when_modified(Dir.pwd, "/path/to/other/file.rb") {|watched_item, file_monitor| ... }
fm.pid            # => 23994
fm.callback.nil?  # => false
fm.watched.size   # => 28


65
66
67
68
69
70
# File 'lib/FileMonitor.rb', line 65

def self.when_modified(*paths, &callback)
  fm = FileMonitor.new &callback
  paths.each {|path| fm << path}
  fm.spawn
  return fm
end

Instance Method Details

#<<(path, regexp_file_filter = /.*/) ⇒ Object

The ‘<<’ method works the same way as the ‘add’ method but does not support a callback.

Example:

fm = FileMonitor.new do |path|
  puts "Detected a change on #{path}"
end

# The following will run the default callback when changes are found in the /tmp folder:
fm << '/tmp'


117
118
119
# File 'lib/FileMonitor.rb', line 117

def <<(path, regexp_file_filter=/.*/)
  add path, regexp_file_filter
end

#add(path, regexp_file_filter = /.*/, &callback) ⇒ Object

The add method accepts a directory path or file path and optional callback. If a directory path is given all files in that path are recursively added. If a callback is given then that proc will be called when a change is detected on that file or group of files. If no proc is given via the add method then the object callback is called. If a regexp is given as the second argument only files matching the regexp will be monitored.

Example:

fm = FileMonitor.new do |path|
  puts "Detected a change on #{path}"
end

# The following will run the default callback when changes are found in the /tmp folder:
fm.add '/tmp'

# The following will run the given callback on any files ending in 'txt' in the /home folder when changed:
fm.add('/home', /txt$/) do |path|
  puts "A users file has changed: #{path}"
end


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/FileMonitor.rb', line 89

def add(path, regexp_file_filter=/.*/, &callback)
  callback = @callback unless callback.is_a? Proc
  # path = ::File.expand_path(path)
  if ::File.file?(path) && regexp_file_filter === ::File.split(path).last
    # Bail out if the file is already being watched.
    return true if index_of(path) 
    index = @watched.size
    @watched[index] = MonitoredItems::Store.new({:path=>::File.expand_path(path), :callback=>callback, :digest=>digest(path)})
    return true
  elsif ::File.directory? path
    files_recursive(path, regexp_file_filter, &callback).each do |f| 
      add f, regexp_file_filter, &callback
    end
    return true
  else
  end
  false
end

#directoriesObject

:nodoc:



247
248
249
# File 'lib/FileMonitor.rb', line 247

def directories #:nodoc:
  @directories ||= []
end

#haltObject

Halts a spawned FileMonitor Instance. The current iteration will be halted in its tracks. See Also: stop

Example:

fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
fm.spawn        # and now its doing its job... 
fm.stop


240
241
242
243
244
245
# File 'lib/FileMonitor.rb', line 240

def halt()
  if Fixnum === @pid
    Process.kill('USR2', @pid)
    Process.wait @pid
  end
end

#index_of(path) ⇒ Object

Returns index of watched item or false if non existant.

Example:

fm = FileMonitor.new
fm << '/tmp/first.txt'
fm << '/tmp/second.txt'
fm.index_of '/tmp/first.txt'   # => 0
fm.index_of '/tmp/first.txt'   # => 1
fm.index_of '/tmp/woops.txt'   # => false


192
193
194
195
# File 'lib/FileMonitor.rb', line 192

def index_of(path)
  watched.each_with_index {|watched,i| return i if watched.path == path}
  false
end

#monitor(interval = 1) ⇒ Object

Runs an endless loop watching for changes. It will sleep for the given interval between looking for changes. This method is intended to be run in a subprocess or threaded environment. The spawn method calls this method and takes care of the forking and pid management for you.

Example:

fm = FileMonitor.new
fm << '/tmp'
fm.monitor
puts "will not get here unless a signal is sent to the process which interrupts the loop."


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/FileMonitor.rb', line 156

def monitor(interval = 1)
  trap("INT") do 
    puts "  FileMonitor was interrupted by Control-C... exiting gracefully"
    # exit
    @shutdown = true
  end
  
  trap("USR1") do
    puts "  FileMonitor was asked nicely to stop."
    @shutdown = true
    pid = nil
  end

  trap("USR2") do
    puts "  FileMonitor was halted."
    pid = nil
    exit
  end

  
  while true
    exit if @shutdown
    process
    sleep interval unless @shutdown
  end
end

#processObject

Itterates watched files and runs callbacks when changes are detected. This is the semi-automatic way to run the FileMonitor.

Example:

changed_files = []
fm = FileMonitor.new() {|watched_item| changed_files = watched_item.path}
fm << '/tmp'
fm.process   # this will look for changes in any watched items only once... call this when you want to look for changes.


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/FileMonitor.rb', line 128

def process
  scan_directories if @options[:rescan_directories]

  @watched.each do |i|
    # Unless the persistant option is set, this will remove watched file if it has been removed
    # if the file still exists then it will be processed regardless of the persistent option.
    unless @options[:persistent] || ::File.exists?(i.path) 
      @watched.delete(i)
    else
      key = digest(i.path)
      # i.digest =  key if i.digest.nil?  # skip first change detection, its always unknown on first run
    
      unless i.digest == key
        respond_to_change(i, key) 
      end
    end
  end
end

#spawn(interval = 1) ⇒ Object Also known as: start

Spauns a child process that is looking for changes at every given interval.

The interval is in seconds and defaults to 1 second.

Example:

fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
fm << @app_root + '/lib'
fm.spawn        # and now its doing its job...


204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/FileMonitor.rb', line 204

def spawn(interval = 1)
  if @pid.nil? 
    @pid = fork {monitor interval}
    Process.detach(pid)
    
    Kernel.at_exit do
      # sends the kill command unless the pid is not found on the system
      Process.kill('HUP', @pid) if process_running?
      @pid = nil
    end
  end
  @pid
end

#stopObject

Stops a spawned FileMonitor instance. The FileMonitor will finish the the currnet iteration and exit gracefully. See Also: Halt

Example:

fm = FileMonitor.new {|watched_item| puts 'do something when file is changed'}
fm.spawn        # and now its doing its job...
fm.stop


224
225
226
227
228
229
230
231
232
# File 'lib/FileMonitor.rb', line 224

def stop()
  # Send user defined signal USR1 to process.  This is trapped in spauned processes and tells the process to Ctrl+C
  # The user defined signial is sent as a safty percausion because the process id is not tracked through a pid file
  # nor compared with the running command.  The FileMonitor spaun will respond to the USR1 signal by exiting properly.*
  if Fixnum === @pid
    Process.kill('USR1', @pid)
    Process.wait @pid
  end
end