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.3'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&callback) ⇒ FileMonitor

The new method may be called with an optional callback which must be a block either do…end or …. The block may consiste of upto 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 "I am watched this file: #{watched_item.path}"
  puts "When I find a change I will call watched_item.callback"
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 '' }
end


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

def initialize(&callback)
  @watched = []
  @callback = callback unless callback.nil?
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


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

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

Instance Method Details

#<<(path) ⇒ 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'


109
110
111
# File 'lib/FileMonitor.rb', line 109

def <<(path)
  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


88
89
90
91
92
93
94
95
96
97
98
# File 'lib/FileMonitor.rb', line 88

def add(path, regexp_file_filter=/.*/, &callback)
  if File.file?(path) && regexp_file_filter === File.split(path).last
    index = index_of(path) || @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).each {|f| add(f, regexp_file_filter, &callback) }
    return true
  end
  false
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


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

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


176
177
178
179
# File 'lib/FileMonitor.rb', line 176

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."


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/FileMonitor.rb', line 140

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.


120
121
122
123
124
125
126
127
128
129
# File 'lib/FileMonitor.rb', line 120

def process
  @watched.each do |i|
    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

#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...


188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/FileMonitor.rb', line 188

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


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

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