directory_watcher-1.5.1/0000755000004100000410000000000012127515724015275 5ustar www-datawww-datadirectory_watcher-1.5.1/version.txt0000644000004100000410000000000612127515724017517 0ustar www-datawww-data1.5.1 directory_watcher-1.5.1/Rakefile0000644000004100000410000000177612127515724016755 0ustar www-datawww-data begin require 'bones' rescue LoadError abort '### please install the "bones" gem ###' end task :default => 'spec:run' #task 'gem:release' => 'spec:run' Bones { name 'directory_watcher' summary 'A class for watching files within a directory and generating events when those files change' authors ['Tim Pease', 'Jeremy Hinegardner'] email 'tim.pease@gmail.com' url 'http://rubygems.org/gems/directory_watcher' spec.opts << "--color" << "--format documentation" # these are optional dependencies for runtime, adding one of them will provide # additional Scanner backends. depend_on 'rev' , :development => true depend_on 'eventmachine', :development => true depend_on 'cool.io' , :development => true depend_on 'bones-git' , '~> 1.2.4', :development => true depend_on 'bones-rspec', '~> 2.0.1', :development => true depend_on 'rspec' , '~> 2.7.0', :development => true depend_on 'logging' , '~> 1.6.1', :development => true } directory_watcher-1.5.1/History.txt0000644000004100000410000000304712127515724017503 0ustar www-datawww-data== Next Version / 2011-XX-XX Major Enhancements - tests! - major refactor Minor Enhancement - events generated from full scans may be sorted by mtime or size - stat information is propagated into the event == 1.4.1 / 2011-08-29 Minor Enhancements - support for latest 'cool.io' == 1.4.0 / 2011-03-15 Minor Enhancements - added support for 'cool.io' [Jonathan Stott] == 1.3.2 / 2010-04-09 * 1 bug fix - removing the now defunct "tasks" folder from the deployed gem == 1.3.1 / 2009-10-26 * 1 bug fix - explicitly killing the Rev loop when stopping == 1.3.0 / 2009-10-21 * 2 major enhancements - added support for Rev based notifications - added support for EventMachine based notifications * 1 minor enhancement - pulled out the scanner thread into its own class == 1.2.0 / 2009-04-12 * 2 minor enhancements - added an option to persist state to a file [Benjamin Thomas] - the option to run the directory watcher scanner manually == 1.1.2 / 2008-12-14 * 1 minor bugfix - fixed directory creation if the watched directory did not exist == 1.1.1 / 2008-01-01 * 1 minor enhancement - now using Mr Bones framework instead of Hoe == 1.1.0 / 2007-11-28 * directory watcher now works with Ruby 1.9 == 1.0.0 / 2007-08-21 * added a join method (much like Thread#join) == 0.1.4 / 2007-08-20 * added version information to the class == 0.1.3 / 2006-12-07 * fixed documentation generation bug == 0.1.2 / 2006-11-26 * fixed warnings == 0.1.1 / 2006-11-10 * removed explicit dependency on hoe == 0.1.0 / 2006-11-10 * initial release directory_watcher-1.5.1/spec/0000755000004100000410000000000012127515724016227 5ustar www-datawww-datadirectory_watcher-1.5.1/spec/directory_watcher_spec.rb0000644000004100000410000000372612127515724023317 0ustar www-datawww-datarequire 'spec_helper' describe DirectoryWatcher do it "has a version" do DirectoryWatcher.version.should =~ /\d\.\d\.\d/ end end describe "Scanners" do [ nil, :em, :coolio ].each do |scanner| # [ :rev ].each do |scanner| context "#{scanner} Scanner" do let( :default_options ) { { :glob => "**/*", :interval => 0.05} } let( :options ) { default_options.merge( :scanner => scanner ) } let( :options_with_pre_load ) { options.merge( :pre_load => true ) } let( :options_with_stable ) { options.merge( :stable => 2 ) } let( :options_with_glob ) { options.merge( :glob => '**/*.42' ) } let( :options_with_persist ) { options.merge( :persist => scratch_path( 'persist.yml' ) ) } let( :directory_watcher ) { DirectoryWatcher.new( @scratch_dir, options ) } let( :directory_watcher_with_pre_load ) { DirectoryWatcher.new( @scratch_dir, options_with_pre_load ) } let( :directory_watcher_with_stable ) { DirectoryWatcher.new( @scratch_dir, options_with_stable ) } let( :directory_watcher_with_glob ) { DirectoryWatcher.new( @scratch_dir, options_with_glob ) } let( :directory_watcher_with_persist ) { DirectoryWatcher.new( @scratch_dir, options_with_persist ) } let( :scenario ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher) } let( :scenario_with_pre_load ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_pre_load ) } let( :scenario_with_stable ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_stable ) } let( :scenario_with_glob ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_glob ) } let( :scenario_with_persist ) { DirectoryWatcherSpecs::Scenario.new( directory_watcher_with_persist ) } it_should_behave_like 'Scanner' end end end directory_watcher-1.5.1/spec/scanner_scenarios.rb0000644000004100000410000001721412127515724022260 0ustar www-datawww-datashared_examples_for "Scanner" do context "Event Types"do it "sends added events" do scenario.run_and_wait_for_event_count(1) do touch( scratch_path( 'added' ) ) end.stop scenario.events.should be_events_like( [[ :added, 'added' ]] ) end it "sends modified events for file size modifications" do modified_file = scratch_path( 'modified' ) scenario.run_and_wait_for_event_count(1) do touch( modified_file ) end.run_and_wait_for_event_count(1) do append_to( modified_file ) end.stop scenario.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified']] ) end it "sends modified events for mtime modifications" do modified_file = scratch_path( 'modified' ) scenario.run_and_wait_for_event_count(1) do touch( modified_file, Time.now - 5 ) end.run_and_wait_for_event_count(1) do touch( modified_file ) end.stop scenario.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified']] ) end it "sends removed events" do removed_file = scratch_path( 'removed' ) scenario.run_and_wait_for_event_count(1) do touch( removed_file, Time.now ) end.run_and_wait_for_event_count(1) do File.unlink( removed_file ) end.stop scenario.events.should be_events_like [ [:added, 'removed'], [:removed, 'removed'] ] end it "sends stable events" do stable_file = scratch_path( 'stable' ) scenario_with_stable.run_and_wait_for_event_count(2) do |s| touch( stable_file ) # do nothing wait for the stable event. end.stop scenario_with_stable.events.should be_events_like [ [:added, 'stable'], [:stable, 'stable'] ] end it "only sends stable events once" do stable_file = scratch_path( 'stable' ) scenario_with_stable.run_and_wait_for_scan_count(5) do |s| touch( stable_file ) # do nothing end.stop scenario_with_stable.events.size.should == 2 end it "events are not sent for directory creation" do a_dir = scratch_path( 'subdir' ) scenario.run_and_wait_for_scan_count(2) do Dir.mkdir( a_dir ) end.stop scenario.events.should be_empty end it "sends events for files in sub directories" do a_dir = scratch_path( 'subdir' ) scenario.run_and_wait_for_event_count(1) do Dir.mkdir( a_dir ) subfile = File.join( a_dir, 'subfile' ) touch( subfile ) end.stop scenario.events.should be_events_like [ [:added, 'subfile'] ] end end context "run_once" do it "can be run on command via 'run_once'" do one_shot_file = scratch_path( "run_once" ) scenario.run_once_and_wait_for_event_count(1) do touch( one_shot_file ) end.stop scenario.events.should be_events_like [ [:added, 'run_once'] ] end end context "pre_load option " do it "skips initial add events" do modified_file = scratch_path( 'modified' ) touch( modified_file, Time.now - 5 ) scenario_with_pre_load.run_and_wait_for_event_count(1) do touch( modified_file ) end.stop scenario_with_pre_load.events.should be_events_like( [[ :modified, 'modified']] ) end end context "globbing" do it "only sends events for files that match" do non_matching = scratch_path( 'no-match' ) matching = scratch_path( 'match.42' ) scenario_with_glob.run_and_wait_for_event_count(1) do touch( non_matching ) touch( matching, Time.now - 5 ) end.run_and_wait_for_event_count(1) do touch( matching ) end.stop scenario_with_glob.events.should be_events_like( [[ :added, 'match.42' ], [ :modified, 'match.42' ]] ) end end context "running?" do it "is true when the watcher is running" do directory_watcher.start directory_watcher.running?.should be_true directory_watcher.stop end it "is false when the watcher is not running" do directory_watcher.running?.should be_false directory_watcher.start directory_watcher.running?.should be_true directory_watcher.stop directory_watcher.running?.should be_false end end context "persistence" do it "saves the current state of the system when the watcher is stopped" do modified_file = scratch_path( 'modified' ) scenario_with_persist.run_and_wait_for_event_count(1) do touch( modified_file, Time.now - 20 ) end.run_and_wait_for_event_count(1) do touch( modified_file, Time.now - 10 ) end.stop scenario_with_persist.events.should be_events_like( [[ :added, 'modified'], [ :modified, 'modified' ]] ) scenario_with_persist.reset scenario_with_persist.resume Thread.pass until scenario_with_persist.events.size >= 1 scenario_with_persist.pause scenario_with_persist.run_and_wait_for_event_count(1) do touch( modified_file ) end.stop scenario_with_persist.events.should be_events_like( [[:added, 'persist.yml'], [ :modified, 'modified' ]] ) end end context "sorting" do [:ascending, :descending].each do |ordering| context "#{ordering}" do context "file name" do let( :filenames ) { ('a'..'z').sort_by {rand} } let( :options ) { default_options.merge( :order_by => ordering ) } before do filenames.each do |p| touch( scratch_path( p )) end end it "#{ordering}" do scenario.run_and_wait_for_event_count(filenames.size) do # wait end final_events = filenames.sort.map { |p| [:added, p] } final_events.reverse! if ordering == :descending scenario.events.should be_events_like( final_events ) end end context "mtime" do let( :current_time ) { Time.now } let( :basenames ) { ('a'..'z').to_a } let( :delta_times ) { unique_integer_list( basenames.size, 5000 ) } let( :filenames ) { basenames.inject({}) { |h,k| h[k] = current_time - delta_times.shift; h } } let( :options ) { default_options.merge( :sort_by => :mtime, :order_by => ordering ) } before do filenames.keys.sort_by{ rand }.each do |p| touch( scratch_path(p), filenames[p] ) end end it "#{ordering}" do scenario.run_and_wait_for_event_count(filenames.size) { nil } sorted_fnames = filenames.to_a.sort_by { |v| v[1] } final_events = sorted_fnames.map { |fn,ts| [:added, fn] } final_events.reverse! if ordering == :descending scenario.events.should be_events_like( final_events ) end end context "size" do let( :basenames ) { ('a'..'z').to_a } let( :file_sizes ) { unique_integer_list( basenames.size, 1000 ) } let( :filenames ) { basenames.inject({}) { |h,k| h[k] = file_sizes.shift; h } } let( :options ) { default_options.merge( :sort_by => :size, :order_by => ordering ) } before do filenames.keys.sort_by{ rand }.each do |p| append_to( scratch_path(p), filenames[p] ) end end it "#{ordering}" do scenario.run_and_wait_for_event_count(filenames.size) { nil } sorted_fnames = filenames.to_a.sort_by { |v| v[1] } final_events = sorted_fnames.map { |fn,ts| [:added, fn] } final_events.reverse! if ordering == :descending scenario.events.should be_events_like( final_events ) end end end end end end directory_watcher-1.5.1/spec/paths_spec.rb0000644000004100000410000000030212127515724020700 0ustar www-datawww-datarequire 'spec_helper' describe DirectoryWatcher::Paths do it "has a libpath" do DirectoryWatcher.lib_path.should == File.expand_path( "../../lib", __FILE__) + ::File::SEPARATOR end end directory_watcher-1.5.1/spec/utility_classes.rb0000644000004100000410000000545312127515724022003 0ustar www-datawww-datamodule DirectoryWatcherSpecs # EventObserver just hangs out and collects all the events that are sent to it # It is used by the Scenario class EventObserver attr_reader :events attr_reader :logger def initialize( logger ) @logger = logger @events = [] end def update( *event_list ) logger.debug "got event #{event_list}" @events.concat event_list end end # Scenario is a utility to wrap up how to run a directory watcher scenario. # You would use it as such: # # dws = Scenario.new( watcher ) # dws.do_after_events(2) do |scenario| # # do something # end.until_events(1) # # This will create a scenario, run the block after 2 events have been # collected, and then return again after 1 more event has been collected. # # You can then check the events with the custom matcher # # dws.events.should be_events_like( ... ) # class Scenario include DirectoryWatcher::Logable attr_reader :watcher def initialize( watcher ) @watcher = watcher @config = watcher.config @observer = EventObserver.new( logger ) @watcher.add_observer( @observer ) reset end def run_and_wait_for_event_count(count, &block ) before_count = @observer.events.size @watcher.resume logger.debug "Before yielding event_count = #{before_count}" logger.debug @observer.events.inspect yield self wait_for_events( before_count + count ) return self end def run_and_wait_for_scan_count(count, &block) @watcher.resume yield self wait_for_scan_count( count ) return self end def events @observer.events end def stop @watcher.stop end def pause @watcher.pause end def resume @watcher.resume end def reset @observer.events.clear @watcher.start @watcher.pause logger.debug "Scenario#reset with pause" end def run_once_and_wait_for_event_count( count, &block ) @watcher.resume @watcher.stop before_count = @observer.events.size yield self @watcher.run_once wait_for_events( before_count + count ) return self end private def wait_for_events( limit ) #Thread.pass until @observer.events.size >= limit until @observer.events.size >= limit do Thread.pass sleep(0.01) logger.debug "Waiting for #{limit} events, I have #{@observer.events.size}" end end def wait_for_scan_count( limit ) @watcher.maximum_iterations = limit #Thread.pass until @watcher.finished_scans? until @watcher.finished_scans? sleep(0.01) logger.debug "Waiting for scan count #{limit} got #{@watcher.scans} #{@watcher.maximum_iterations}" end end end end directory_watcher-1.5.1/spec/spec_helper.rb0000644000004100000410000000433612127515724021053 0ustar www-datawww-data require File.expand_path('../../lib/directory_watcher', __FILE__) require 'logging' require 'rspec/logging_helper' require 'rspec/autorun' require 'scanner_scenarios' require 'utility_classes' include Logging.globally #Thread.abort_on_exception = true module DirectoryWatcherSpecs::Helpers def scratch_path( *parts ) File.join( @scratch_dir, *parts ) end # NOTE : touch will only work on *nix/BSD style systems # Touch the file with the given timestamp def touch( fname, time = Time.now ) stamp = time.strftime("%Y%m%d%H%M.%S") %x[ touch -m -t #{stamp} #{fname} ] end def append_to( fname, count = 1 ) File.open( fname, "a" ) { |f| count.times { f.puts Time.now }} end # create a unique list of numbers with size 'count' and from the range # 0..range def unique_integer_list( count, range ) random = (0..range).to_a.sort_by { rand } return random[0,count] end end RSpec.configure do |config| config.before(:each) do @spec_dir = DirectoryWatcher.sub_path( "spec" ) @scratch_dir = File.join(@spec_dir, "scratch") FileUtils.rm_rf @scratch_dir if File.directory?( @scratch_dir ) FileUtils.mkdir @scratch_dir unless File.directory?( @scratch_dir ) end config.after(:each) do FileUtils.rm_rf @scratch_dir if File.directory?(@scratch_dir) end config.include DirectoryWatcherSpecs::Helpers include RSpec::LoggingHelper config.capture_log_messages end RSpec::Matchers.define :be_events_like do |expected| match do |actual| a = actual.kind_of?( Array ) ? actual.map {|e| [ e.type, File.basename( e.path ) ]} : [ actual.type, File.basename( actual.path ) ] a == expected end failure_message_for_should do |actual| s = StringIO.new s.puts [ "Actual".ljust(20), "Expected".ljust(20), "Same?".ljust(20) ].join(" ") s.puts [ "-"*20, "-"*20, "-"*20 ].join(" ") [ actual.size, expected.size ].max.times do |x| a = actual[x] a = a.kind_of?( Array ) ? a.map {|e| [ e.type, File.basename( e.path ) ]} : [ a.type, File.basename( a.path ) ] e = expected[x] r = (a == e) ? "OK" : "Differ" s.puts [ a.inspect.ljust(20), e.inspect.ljust(20), r ].join(" ") end s.string end end directory_watcher-1.5.1/metadata.yml0000644000004100000410000001243712127515724017607 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: directory_watcher version: !ruby/object:Gem::Version version: 1.5.1 prerelease: platform: ruby authors: - Tim Pease - Jeremy Hinegardner autorequire: bindir: bin cert_chain: [] date: 2013-03-20 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rev requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 0.3.2 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 0.3.2 - !ruby/object:Gem::Dependency name: eventmachine requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.0.3 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.0.3 - !ruby/object:Gem::Dependency name: cool.io requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.1.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.1.0 - !ruby/object:Gem::Dependency name: bones-git requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 1.2.4 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 1.2.4 - !ruby/object:Gem::Dependency name: bones-rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 2.0.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 2.0.1 - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 2.7.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 2.7.0 - !ruby/object:Gem::Dependency name: logging requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 1.6.1 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: 1.6.1 - !ruby/object:Gem::Dependency name: bones requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 3.8.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 3.8.0 description: ! 'The directory watcher operates by scanning a directory at some interval and generating a list of files based on a user supplied glob pattern. As the file list changes from one interval to the next, events are generated and dispatched to registered observers. Three types of events are supported -- added, modified, and removed.' email: tim.pease@gmail.com executables: [] extensions: [] extra_rdoc_files: - History.txt - README.txt files: - .gitignore - History.txt - README.txt - Rakefile - lib/directory_watcher.rb - lib/directory_watcher/collector.rb - lib/directory_watcher/configuration.rb - lib/directory_watcher/coolio_scanner.rb - lib/directory_watcher/em_scanner.rb - lib/directory_watcher/event.rb - lib/directory_watcher/eventable_scanner.rb - lib/directory_watcher/file_stat.rb - lib/directory_watcher/logable.rb - lib/directory_watcher/notifier.rb - lib/directory_watcher/paths.rb - lib/directory_watcher/rev_scanner.rb - lib/directory_watcher/scan.rb - lib/directory_watcher/scan_and_queue.rb - lib/directory_watcher/scanner.rb - lib/directory_watcher/threaded.rb - lib/directory_watcher/version.rb - spec/directory_watcher_spec.rb - spec/paths_spec.rb - spec/scanner_scenarios.rb - spec/spec_helper.rb - spec/utility_classes.rb - version.txt homepage: http://rubygems.org/gems/directory_watcher licenses: [] post_install_message: rdoc_options: - --main - README.txt require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: directory_watcher rubygems_version: 1.8.23 signing_key: specification_version: 3 summary: A class for watching files within a directory and generating events when those files change test_files: [] directory_watcher-1.5.1/.gitignore0000644000004100000410000000051212127515724017263 0ustar www-datawww-data# git-ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~ announcement.txt coverage doc pkg tags temp.dir tmp.rb tmp.yml vendor script spec/scratch directory_watcher-1.5.1/README.txt0000644000004100000410000000424512127515724017000 0ustar www-datawww-dataDirectory Watcher by Tim Pease http://codeforpeople.rubyforge.org/directory_watcher == DESCRIPTION: The directory watcher operates by scanning a directory at some interval and generating a list of files based on a user supplied glob pattern. As the file list changes from one interval to the next, events are generated and dispatched to registered observers. Three types of events are supported -- added, modified, and removed. == FEATURES: See DirectoryWatcher for detailed documentation and usage. == REQUIREMENTS: This is a pure ruby library. There are no requirements for using this code. == INSTALL: sudo gem install directory_watcher You will need have Mr Bones installed in order to develop or modify directory watcher. sudo gem install bones == NOTES: The support for EventMachine based file notifications is fairly new and experimental. Please feel free to experiment and report any issues on the github issue tracker. http://github.com/TwP/directory_watcher/issues The support for Rev based file notifications is also fairly new and subject to the same disclaimer as the EventMachine functionality. == LICENSE: MIT License Copyright (c) 2007 - 2013 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. directory_watcher-1.5.1/lib/0000755000004100000410000000000012127515724016043 5ustar www-datawww-datadirectory_watcher-1.5.1/lib/directory_watcher.rb0000644000004100000410000004750612127515724022125 0ustar www-datawww-data# # = directory_watcher.rb # # See DirectoryWatcher for detailed documentation and usage. # require 'set' require 'thread' require 'yaml' require 'directory_watcher/paths' require 'directory_watcher/version' require 'directory_watcher/configuration' require 'directory_watcher/logable' # == Synopsis # # A class for watching files within a directory and generating events when # those files change. # # == Details # # A directory watcher is an +Observable+ object that sends events to # registered observers when file changes are detected within the directory # being watched. # # The directory watcher operates by scanning the directory at some interval # and creating a list of the files it finds. File events are detected by # comparing the current file list with the file list from the previous scan # interval. Three types of events are supported -- *added*, *modified*, and # *removed*. # # An added event is generated when the file appears in the current file # list but not in the previous scan interval file list. A removed event is # generated when the file appears in the previous scan interval file list # but not in the current file list. A modified event is generated when the # file appears in the current and the previous interval file list, but the # file modification time or the file size differs between the two lists. # # The file events are collected into an array, and all registered observers # receive all file events for each scan interval. It is up to the individual # observers to filter the events they are interested in. # # === File Selection # # The directory watcher uses glob patterns to select the files to scan. The # default glob pattern will select all regular files in the directory of # interest '*'. # # Here are a few useful glob examples: # # '*' => all files in the current directory # '**/*' => all files in all subdirectories # '**/*.rb' => all ruby files # 'ext/**/*.{h,c}' => all C source code files # # *Note*: file events will never be generated for directories. Only regular # files are included in the file scan. # # === Stable Files # # A fourth file event is supported but not enabled by default -- the # *stable* event. This event is generated after a file has been added or # modified and then remains unchanged for a certain number of scan # intervals. # # To enable the generation of this event the +stable+ count must be # configured. This is the number of scan intervals a file must remain # unchanged (based modification time and file size) before it is considered # stable. # # To disable this event the +stable+ count should be set to +nil+. # # == Usage # # Learn by Doing -- here are a few different ways to configure and use a # directory watcher. # # === Basic # # This basic recipe will watch all files in the current directory and # generate the three default events. We'll register an observer that simply # prints the events to standard out. # # require 'directory_watcher' # # dw = DirectoryWatcher.new '.' # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.start # gets # when the user hits "enter" the script will terminate # dw.stop # # === Suppress Initial "added" Events # # This little twist will suppress the initial "added" events that are # generated the first time the directory is scanned. This is done by # pre-loading the watcher with files -- i.e. telling the watcher to scan for # files before actually starting the scan loop. # # require 'directory_watcher' # # dw = DirectoryWatcher.new '.', :pre_load => true # dw.glob = '**/*.rb' # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.start # gets # when the user hits "enter" the script will terminate # dw.stop # # There is one catch with this recipe. The glob pattern must be specified # before the pre-load takes place. The glob pattern can be given as an # option to the constructor: # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :pre_load => true # # The other option is to use the reset method: # # dw = DirectoryWatcher.new '.' # dw.glob = '**/*.rb' # dw.reset true # the +true+ flag causes the watcher to pre-load # # the files # # === Generate "stable" Events # # In order to generate stable events, the stable count must be specified. In # this example the interval is set to 5.0 seconds and the stable count is # set to 2. Stable events will only be generated for files after they have # remain unchanged for 10 seconds (5.0 * 2). # # require 'directory_watcher' # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb' # dw.interval = 5.0 # dw.stable = 2 # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.start # gets # when the user hits "enter" the script will terminate # dw.stop # # === Persisting State # # A directory watcher can be configured to persist its current state to a # file when it is stopped and to load state from that same file when it # starts. Setting the +persist+ value to a filename will enable this # feature. # # require 'directory_watcher' # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb' # dw.interval = 5.0 # dw.persist = "dw_state.yml" # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.start # loads state from dw_state.yml # gets # when the user hits "enter" the script will terminate # dw.stop # stores state to dw_state.yml # # === Running Once # # Instead of using the built in run loop, the directory watcher can be run # one or many times using the +run_once+ method. The state of the directory # watcher can be loaded and dumped if so desired. # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb' # dw.persist = "dw_state.yml" # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.load! # loads state from dw_state.yml # dw.run_once # sleep 5.0 # dw.run_once # dw.persist! # stores state to dw_state.yml # # === Ordering of Events # # In the case, particularly in the initial scan, or in cases where the Scanner # may be doing a large pass over the monitored locations, many events may be # generated all at once. In the default case, these will be emitted in the order # in which they are observed, which tends to be alphabetical, but it not # guaranteed. If you wish the events to be order by modified time, or file size # this may be done by setting the +sort_by+ and/or the +order_by+ options. # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :sort_by => :mtime # dw.add_observer {|*args| args.each {|event| puts event}} # dw.start # gets # when the user hits "enter" the script will terminate # dw.stop # # === Scanning Strategies # # By default DirectoryWatcher uses a thread that scans the directory being # watched for files and calls "stat" on each file. The stat information is # used to determine which files have been modified, added, removed, etc. # This approach is fairly intensive for short intervals and/or directories # with many files. # # DirectoryWatcher supports using Cool.io, EventMachine, or Rev instead # of a busy polling thread. These libraries use system level kernel hooks to # receive notifications of file system changes. This makes DirectoryWorker # much more efficient. # # This example will use Cool.io to generate file notifications. # # dw = DirectoryWatcher.new '.', :glob => '**/*.rb', :scanner => :coolio # dw.add_observer {|*args| args.each {|event| puts event}} # # dw.start # gets # when the user hits "enter" the script will terminate # dw.stop # # The scanner cannot be changed after the DirectoryWatcher has been # created. To use an EventMachine scanner, pass :em as the :scanner # option. # # If you wish to use the Cool.io scanner, then you must have the Cool.io gem # installed. The same goes for EventMachine and Rev. To install any of these # gems run the following on the command line: # # gem install cool.io # gem install eventmachine # gem install rev # # Note: Rev has been replace by Cool.io and support for the Rev scanner will # eventually be dropped from DirectoryWatcher. # # == Contact # # A lot of discussion happens about Ruby in general on the ruby-talk # mailing list (http://www.ruby-lang.org/en/ml.html), and you can ask # any questions you might have there. I monitor the list, as do many # other helpful Rubyists, and you're sure to get a quick answer. Of # course, you're also welcome to email me (Tim Pease) directly at the # at tim.pease@gmail.com, and I'll do my best to help you out. # # (the above paragraph was blatantly stolen from Nathaniel Talbott's # Test::Unit documentation) # # == Author # # Tim Pease # class DirectoryWatcher extend Paths extend Version include Logable # access the configuration of the DirectoryWatcher attr_reader :config # call-seq: # DirectoryWatcher.new( directory, options ) # # Create a new +DirectoryWatcher+ that will generate events when file # changes are detected in the given _directory_. If the _directory_ does # not exist, it will be created. The following options can be passed to # this method: # # :glob => '*' file glob pattern to restrict scanning # :interval => 30.0 the directory scan interval (in seconds) # :stable => nil the number of intervals a file must remain # unchanged for it to be considered "stable" # :pre_load => false setting this option to true will pre-load the # file list effectively skipping the initial # round of file added events that would normally # be generated (glob pattern must also be # specified otherwise odd things will happen) # :persist => file the state will be persisted to and restored # from the file when the directory watcher is # stopped and started (respectively) # :scanner => nil the directory scanning strategy to use with # the directory watcher (either :coolio, :em, :rev or nil) # :sort_by => :path the sort order of the scans, when there are # multiple events ready for deliver. This can be # one of: # # :path => default, order by file name # :mtime => order by last modified time # :size => order by file size # :order_by => :ascending The direction in which the sorted items are # sorted. Either :ascending or :descending # :logger => nil An object that responds to the debug, info, warn, # error and fatal methods. Using the default will # use Logging gem if it is available and then fall # back to NullLogger # # The default glob pattern will scan all files in the configured directory. # Setting the :stable option to +nil+ will prevent stable events from being # generated. # # Additional information about the available options is documented in the # Configuration class. # def initialize( directory, opts = {} ) @observer_peers = {} @config = Configuration.new( opts.merge( :dir => directory ) ) setup_dir(config.dir) @notifier = Notifier.new(config, @observer_peers) @collector = Collector.new(config) @scanner = config.scanner_class.new(config) end # Setup the directory existence. # # Raise an error if the item passed in does exist but is not a directory # # Returns nothing def setup_dir( dir ) if Kernel.test(?e, dir) unless Kernel.test(?d, dir) raise ArgumentError, "'#{dir}' is not a directory" end else Dir.mkdir dir end end # call-seq: # add_observer( observer, func = :update ) # add_observer {|*events| block} # # Adds the given _observer_ as an observer on this directory watcher. The # _observer_ will now receive file events when they are generated. The # second optional argument specifies a method to notify updates, of which # the default value is +update+. # # Optionally, a block can be passed as the observer. The block will be # executed with the file events passed as the arguments. A reference to the # underlying +Proc+ object will be returned for use with the # +delete_observer+ method. # def add_observer( observer = nil, func = :update, &block ) unless block.nil? observer = block.to_proc func = :call end unless observer.respond_to? func raise NoMethodError, "observer does not respond to `#{func.to_s}'" end logger.debug "Added observer" @observer_peers[observer] = func observer end # Delete +observer+ as an observer of this directory watcher. It will no # longer receive notifications. # def delete_observer( observer ) @observer_peers.delete observer end # Delete all observers associated with the directory watcher. # def delete_observers @observer_peers.clear end # Return the number of observers associated with this directory watcher.. # def count_observers @observer_peers.size end # call-seq: # glob = '*' # glob = ['lib/**/*.rb', 'test/**/*.rb'] # # Sets the glob pattern that will be used when scanning the directory for # files. A single glob pattern can be given or an array of glob patterns. # def glob=( val ) config.glob = val end def glob config.glob end # Sets the directory scan interval. The directory will be scanned every # _interval_ seconds for changes to files matching the glob pattern. # Raises +ArgumentError+ if the interval is zero or negative. # def interval=( val ) config.interval = val end def interval config.interval end # Sets the number of intervals a file must remain unchanged before it is # considered "stable". When this condition is met, a stable event is # generated for the file. If stable is set to +nil+ then stable events # will not be generated. # # A stable event will be generated once for a file. Another stable event # will only be generated after the file has been modified and then remains # unchanged for _stable_ intervals. # # Example: # # dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' ) # dw.interval = 15.0 # dw.stable = 4 # # In this example, a directory watcher is configured to look for swap files # in the /tmp directory. Stable events will be generated every 4 scan # intervals iff a swap remains unchanged for that time. In this case the # time is 60 seconds (15.0 * 4). # def stable=( val ) config.stable = val end def stable config.stable end # Sets the name of the file to which the directory watcher state will be # persisted when it is stopped. Setting the persist filename to +nil+ will # disable this feature. # def persist=( filename ) config.persist = filename end def persist config.persist end # Write the current state of the directory watcher to the persist file. # This method will do nothing if the directory watcher is running or if # the persist file is not configured. # def persist! return if running? File.open(persist, 'w') { |fd| @collector.dump_stats(fd) } if persist? self rescue => e logger.error "Failure to write to persitence file #{persist.inspect} : #{e}" end # Is persistence done on this DirectoryWatcher # def persist? config.persist end # Loads the state of the directory watcher from the persist file. This # method will do nothing if the directory watcher is running or if the # persist file is not configured. # def load! return if running? File.open(persist, 'r') { |fd| @collector.load_stats(fd) } if persist? and test(?f, persist) self end # Returns +true+ if the directory watcher is currently running. Returns # +false+ if this is not the case. # def running? @scanner.running? end # Start the directory watcher scanning thread. If the directory watcher is # already running, this method will return without taking any action. # # Start returns one the scanner and the notifier say they are running # def start logger.debug "start (running -> #{running?})" return self if running? load! logger.debug "starting notifier #{@notifier.object_id}" @notifier.start Thread.pass until @notifier.running? logger.debug "starting collector" @collector.start Thread.pass until @collector.running? logger.debug "starting scanner" @scanner.start Thread.pass until @scanner.running? self end # Pauses the scanner. # def pause @scanner.pause end # Resume the emitting of events # def resume @scanner.resume end # Stop the directory watcher scanning thread. If the directory watcher is # already stopped, this method will return without taking any action. # # Stop returns once the scanner and notifier say they are no longer running def stop logger.debug "stop (running -> #{running?})" return self unless running? logger.debug"stopping scanner" @scanner.stop Thread.pass while @scanner.running? logger.debug"stopping collector" @collector.stop Thread.pass while @collector.running? logger.debug"stopping notifier" @notifier.stop Thread.pass while @notifier.running? self ensure persist! end # Sets the maximum number of scans the scanner is to make on the directory # def maximum_iterations=( value ) @scanner.maximum_iterations = value end # Returns the maximum number of scans the directory scanner will perform # def maximum_iterations @scanner.maximum_iterations end # Returns the number of scans of the directory scanner it has # completed thus far. # # This will always report 0 unless a maximum number of scans has been set # def scans @scanner.iterations end # Returns true if the maximum number of scans has been reached. # def finished_scans? return true if maximum_iterations and (scans >= maximum_iterations) return false end # call-seq: # reset( pre_load = false ) # # Reset the directory watcher state by clearing the stored file list. If # the directory watcher is running, it will be stopped, the file list # cleared, and then restarted. Passing +true+ to this method will cause # the file list to be pre-loaded after it has been cleared effectively # skipping the initial round of file added events that would normally be # generated. # def reset( pre_load = false ) was_running = @scanner.running? stop if was_running File.delete(config.persist) if persist? and test(?f, config.persist) @scanner.reset pre_load start if was_running self end # call-seq: # join( limit = nil ) # # If the directory watcher is running, the calling thread will suspend # execution and run the directory watcher thread. This method does not # return until the directory watcher is stopped or until _limit_ seconds # have passed. # # If the directory watcher is not running, this method returns immediately # with +nil+. # def join( limit = nil ) @scanner.join limit end # Performs exactly one scan of the directory for file changes and notifies # the observers. # def run_once @scanner.run @collector.start unless running? @notifier.start unless running? self end end # class DirectoryWatcher require 'directory_watcher/file_stat' require 'directory_watcher/scan' require 'directory_watcher/event' require 'directory_watcher/threaded' require 'directory_watcher/collector' require 'directory_watcher/notifier' require 'directory_watcher/scan_and_queue' require 'directory_watcher/scanner' require 'directory_watcher/eventable_scanner' require 'directory_watcher/coolio_scanner' require 'directory_watcher/em_scanner' require 'directory_watcher/rev_scanner' # EOF directory_watcher-1.5.1/lib/directory_watcher/0000755000004100000410000000000012127515724021564 5ustar www-datawww-datadirectory_watcher-1.5.1/lib/directory_watcher/em_scanner.rb0000644000004100000410000001036512127515724024230 0ustar www-datawww-databegin require 'eventmachine' DirectoryWatcher::HAVE_EM = true rescue LoadError DirectoryWatcher::HAVE_EM = false end if DirectoryWatcher::HAVE_EM # Set up the appropriate polling options [:epoll, :kqueue].each do |poll| if EventMachine.send("#{poll}?") then EventMachine.send("#{poll}=", true ) break end end # The EmScanner uses the EventMachine reactor loop to monitor changes to # files in the watched directory. This scanner is more efficient than the # pure Ruby scanner because it relies on the operating system kernel # notifications instead of a periodic polling and stat of every file in the # watched directory (the technique used by the Scanner class). # # EventMachine cannot notify us when a file is added to the watched # directory; therefore, added files are only picked up when we apply the # glob pattern to the directory. This is done at the configured interval. # # Notes: # # * Kqueue does not generate notifications when "touch" is used to update # a file's timestamp. This applies to Mac and BSD systems. # # * New files are detected only when the watched directory is polled at the # configured interval. # class DirectoryWatcher::EmScanner < DirectoryWatcher::EventableScanner # call-seq: # EmScanner.new( configuration ) # def initialize( config ) super(config) end # Called by EventablScanner#start to start the loop up and attach the periodic # timer that will poll the globs for new files. # def start_loop_with_attached_scan_timer return if @loop_thread unless EventMachine.reactor_running? @loop_thread = Thread.new {EventMachine.run} Thread.pass until EventMachine.reactor_running? end @timer = ScanTimer.new(self) end # Called by EventableScanner#stop to stop the loop as part of the shutdown # process. # def stop_loop if @loop_thread then EventMachine.next_tick do EventMachine.stop_event_loop end @loop_thread.kill @loop_thread = nil end end # :stopdoc: # # This is our tailored implementation of the EventMachine FileWatch class. # It receives notifications of file events and provides a mechanism to # translate the EventMachine events into objects to send to the Scanner that # it is initialized with. # # The Watcher only responds to modified and deleted events. # # This class is required by EventableScanner to institute file watching. # class Watcher < EventMachine::FileWatch def self.watch( path, scanner ) EventMachine.watch_file path, Watcher, scanner end # Initialize the Watcher using with the given scanner # Post initialization, EventMachine will set @path # def initialize( scanner ) @scanner = scanner end # EventMachine callback for when a watched file is deleted. We convert this # to a FileStat object for a removed file. # def file_deleted @scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(@path)) end # Event Machine also sends events on file_moved which we'll just consider a # file deleted and the file added event will be picked up by the next scan alias :file_moved :file_deleted # EventMachine callback for when a watched file is modified. We convert this # to a FileStat object and send it to the collector def file_modified stat = File.stat @path @scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(@path, stat.mtime, stat.size)) end # Detach the watcher from the event loop. # # Required by EventableScanner as part of the shutdown process. # def detach EventMachine.next_tick do stop_watching end end end # Periodically execute a Scan. # # This object is used by EventableScanner to during shutdown. # class ScanTimer def initialize( scanner ) @scanner = scanner @timer = EventMachine::PeriodicTimer.new( @scanner.interval, method(:on_scan) ) end def on_scan @scanner.on_scan end # Detach the watcher from the event loop. # # Required by EventableScanner as part of the shutdown process. # def detach EventMachine.next_tick do @timer.cancel end end end # :startdoc: end # class DirectoryWatcher::EmScanner end # if HAVE_EM # EOF directory_watcher-1.5.1/lib/directory_watcher/version.rb0000644000004100000410000000022612127515724023576 0ustar www-datawww-dataclass DirectoryWatcher module Version def version File.read(DirectoryWatcher.path('version.txt')).strip end extend self end end directory_watcher-1.5.1/lib/directory_watcher/file_stat.rb0000644000004100000410000000355212127515724024070 0ustar www-datawww-data# FileStat contains file system information about a single file including: # # path - The fully expanded path of the file # mtime - The last modified time of the file, as a Time object # size - The size of the file, in bytes. # # The FileStat object can also say if the file is removed of not. # class DirectoryWatcher::FileStat # The fully expanded path of the file attr_reader :path # The last modified time of the file attr_accessor :mtime # The size of the file in bytes attr_accessor :size # Create an instance of FileStat that will make sure that the instance method # +removed?+ returns true when called on it. # def self.for_removed_path( path ) ::DirectoryWatcher::FileStat.new(path, nil, nil) end # Create a new instance of FileStat with the given path, mtime and size # def initialize( path, mtime, size ) @path = path @mtime = mtime @size = size end # Is the file represented by this FileStat to be considered removed? # # FileStat doesn't actually go to the file system and check, it assumes if the # FileStat was initialized with a nil mtime or a nil size then that data # wasn't available, and therefore must indicate that the file is no longer in # existence. # def removed? @mtime.nil? || @size.nil? end # Compare this FileStat to another object. # # This will only return true when all of the following are true: # # 1) The other object is also a FileStat object # 2) The other object's mtime is equal to this mtime # 3) The other object's msize is equal to this size # def eql?( other ) return false unless other.instance_of? self.class self.mtime == other.mtime and self.size == other.size end alias :== :eql? # Create a nice string based representation of this instance. # def to_s "<#{self.class.name} path: #{path} mtime: #{mtime} size: #{size}>" end end directory_watcher-1.5.1/lib/directory_watcher/coolio_scanner.rb0000644000004100000410000000650112127515724025110 0ustar www-datawww-databegin require 'coolio' DirectoryWatcher::HAVE_COOLIO = true rescue LoadError DirectoryWatcher::HAVE_COOLIO = false end if DirectoryWatcher::HAVE_COOLIO # The CoolioScanner uses the Coolio loop to monitor changes to files in the # watched directory. This scanner is more efficient than the pure Ruby # scanner because it relies on the operating system kernel notifications # instead of a periodic polling and stat of every file in the watched # directory (the technique used by the Scanner class). # # Coolio cannot notify us when a file is added to the watched # directory; therefore, added files are only picked up when we apply the # glob pattern to the directory. This is done at the configured interval. # class DirectoryWatcher::CoolioScanner < DirectoryWatcher::EventableScanner # call-seq: # CoolioScanner.new( config ) # def initialize( config ) super(config) end # Called by EventablScanner#start to start the loop up and attach the periodic # timer that will poll the globs for new files. # def start_loop_with_attached_scan_timer return if @loop_thread @timer = ScanTimer.new( self ) @loop_thread = Thread.new { @timer.attach(event_loop) event_loop.run } end # Called by EventableScanner#stop to stop the loop as part of the shutdown # process. # def stop_loop if @loop_thread then event_loop.stop rescue nil @loop_thread.kill @loop_thread = nil end end # Return the cool.io loop object. # # This is used during the startup, shutdown process and for the Watcher to # attach and detach from the event loop # def event_loop if @loop_thread then @loop_thread._coolio_loop else Thread.current._coolio_loop end end # :stopdoc: # # Watch files using the Coolio StatWatcher. # # This class is required by EventableScanner to institute file watching. # # The coolio +on_change+ callback is converted to the appropriate +on_removed+ # and +on_modified+ callbacks for the EventableScanner. # class Watcher < Coolio::StatWatcher def self.watch(fn, scanner ) new(fn, scanner) end def initialize( fn, scanner ) # for file watching, we want to make sure this happens at a reasonable # value, so set it to 0 if the scanner.interval is > 5 seconds. This will # make it use the system value, and allow us to test. i = scanner.interval < 5 ? scanner.interval : 0 super(fn, i) @scanner = scanner attach(scanner.event_loop) end # Cool.io uses on_change so we convert that to the appropriate # EventableScanner calls. # def on_change( prev_stat, current_stat ) logger.debug "on_change called" if File.exist?(path) then @scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(path, current_stat.mtime, current_stat.size)) else @scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(path)) end end end # Periodically execute a Scan. Hook this into the EventableScanner#on_scan # class ScanTimer< Coolio::TimerWatcher def initialize( scanner ) super(scanner.interval, true) @scanner = scanner end def on_timer( *args ) @scanner.on_scan end end # :startdoc: end # class DirectoryWatcher::CoolioScanner end # if DirectoryWatcher::HAVE_COOLIO # EOF directory_watcher-1.5.1/lib/directory_watcher/configuration.rb0000644000004100000410000001633212127515724024765 0ustar www-datawww-data# # The top level configuration options used by DirectoryWatcher are used by many # of the sub components for a variety of purposes. The Configuration represents # all those options and other global like instances. # # The top level DirectoryWatcher class allows the configs to be changed during # execution, so all of the dependent classes need to be informed when their # options have changed. This class allows that. # class DirectoryWatcher::Configuration # The directory to monitor for events. The glob's will be used in conjunction # with this directory to find the full list of globs available. attr_reader :dir # The glob of files to monitor. This is an Array of file matching globs # be aware that changing the :glob value after watching has started has the # potential to cause spurious events if the new globs do not match the old, # files will appear to have been deleted. # # The default is '*' attr_reader :glob # The interval at which to do a full scan using the +glob+ to determine Events # to send. # # The default is 30.0 seconds attr_reader :interval # Controls the number of intervals a file must remain unchanged before it is # considered "stable". When this condition is met, a stable event is # generated for the file. If stable is set to +nil+ then stable events # will not be generated. # # The default is nil, indicating no stable events are to be emitted. attr_reader :stable # pre_load says if an initial scan using the globs should be done to pre # populate the state of the system before sending any events. # # The default is false attr_reader :pre_load # The filename to persist the state of the DirectoryWatcher too upon calling # *stop*. # # The default is nil, indicating that no state is to be persisted. attr_reader :persist # The back end scanner to use. The available options are: # # nil => Use the default, pure ruby Threaded scanner # :em => Use the EventMachine based scanner. This requires that the # 'eventmachine' gem be installed. # :coolio => Use the Cool.io based scanner. This requires that the # 'cool.io' gem be installed. # :rev => Use the Rev based scanner. This requires that the 'rev' gem be # installed. # # The default is nil, indicating the pure ruby threaded scanner will be used. # This option may not be changed once the DirectoryWatcher is allocated. # attr_reader :scanner # The sorting method to use when emitting a set of Events after a Scan has # happened. Since a Scan may produce a number of events, if those Events should # be emitted in a particular order, use +sort_by+ to pick which field to sort # the events, and +order_by+ to say if those events are to be emitted in # :ascending or :descending order. # # Available options: # # :path => The default, they will be sorted by full pathname # :mtime => Last modified time. They will be sorted by their FileStat mtime # :size => The number of bytes in the file. # attr_accessor :sort_by # When sorting you may pick if the order should be: # # :ascending => The default, from lowest to highest # :descending => from highest to lowest. # attr_accessor :order_by # The Queue through which the Scanner will send data to the Collector # attr_reader :collection_queue # The Queue through which the Collector will send data to the Notifier # attr_reader :notification_queue # The logger through wich every one will log # attr_reader :logger # Return a Hash of all the default options # def self.default_options { :dir => '.', :glob => '*', :interval => 30.0, :stable => nil, :pre_load => false, :persist => nil, :scanner => nil, :sort_by => :path, :order_by => :ascending, :logger => nil, } end # Create a new Configuration by blending the passed in items with the defaults # def initialize( options = {} ) o = self.class.default_options.merge( options ) @dir = o[:dir] @pre_load = o[:pre_load] @scanner = o[:scanner] @sort_by = o[:sort_by] @order_by = o[:order_by] # These have validation rules self.persist = o[:persist] self.interval = o[:interval] self.glob = o[:glob] self.stable = o[:stable] self.logger = o[:logger] @notification_queue = Queue.new @collection_queue = Queue.new end # Is pre_load set or not # def pre_load? @pre_load end # The class of the scanner # def scanner_class class_name = scanner.to_s.capitalize + 'Scanner' DirectoryWatcher.const_get( class_name ) rescue DirectoryWatcher::Scanner end # call-seq: # glob = '*' # glob = ['lib/**/*.rb', 'test/**/*.rb'] # # Sets the glob pattern that will be used when scanning the directory for # files. A single glob pattern can be given or an array of glob patterns. # def glob=( val ) glob = case val when String; [File.join(@dir, val)] when Array; val.flatten.map! {|g| File.join(@dir, g)} else raise(ArgumentError, 'expecting a glob pattern or an array of glob patterns') end glob.uniq! @glob = glob end # Sets the directory scan interval. The directory will be scanned every # _interval_ seconds for changes to files matching the glob pattern. # Raises +ArgumentError+ if the interval is zero or negative. # def interval=( val ) val = Float(val) raise ArgumentError, "interval must be greater than zero" if val <= 0 @interval = val end # Sets the logger instance. This will be used by all classes for logging # def logger=( val ) if val then if %w[ debug info warn error fatal ].all? { |meth| val.respond_to?( meth ) } then @logger = val end else @logger = ::DirectoryWatcher::Logable.default_logger end end # Sets the number of intervals a file must remain unchanged before it is # considered "stable". When this condition is met, a stable event is # generated for the file. If stable is set to +nil+ then stable events # will not be generated. # # A stable event will be generated once for a file. Another stable event # will only be generated after the file has been modified and then remains # unchanged for _stable_ intervals. # # Example: # # dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' ) # dw.interval = 15.0 # dw.stable = 4 # # In this example, a directory watcher is configured to look for swap files # in the /tmp directory. Stable events will be generated every 4 scan # intervals iff a swap remains unchanged for that time. In this case the # time is 60 seconds (15.0 * 4). # def stable=( val ) if val.nil? @stable = nil else val = Integer(val) raise ArgumentError, "stable must be greater than zero" if val <= 0 @stable = val end return @stable end # Sets the name of the file to which the directory watcher state will be # persisted when it is stopped. Setting the persist filename to +nil+ will # disable this feature. # def persist=( filename ) @persist = filename ? filename.to_s : nil end end directory_watcher-1.5.1/lib/directory_watcher/event.rb0000644000004100000410000000360212127515724023233 0ustar www-datawww-data# An +Event+ structure contains the _type_ of the event and the file _path_ # to which the event pertains. The type can be one of the following: # # :added => file has been added to the directory # :modified => file has been modified (either mtime or size or both # have changed) # :removed => file has been removed from the directory # :stable => file has stabilized since being added or modified # class DirectoryWatcher::Event attr_reader :type attr_reader :path attr_reader :stat # Create one of the 4 types of events given the two stats # # The rules are: # # :added => old_stat will be nil and new_stat will exist # :removed => old_stat will exist and new_stat will be nil # :modified => old_stat != new_stat # :stable => old_stat == new_stat and # def self.from_stats( old_stat, new_stat ) if old_stat != new_stat then return DirectoryWatcher::Event.new( :removed, new_stat.path ) if new_stat.removed? return DirectoryWatcher::Event.new( :added, new_stat.path, new_stat ) if old_stat.nil? return DirectoryWatcher::Event.new( :modified, new_stat.path, new_stat ) else return DirectoryWatcher::Event.new( :stable, new_stat.path, new_stat ) end end # Create a new Event with one of the 4 types and the path of the file. # def initialize( type, path, stat = nil ) @type = type @path = path @stat = stat end # Is the event a modified event. # def modified? type == :modified end # Is the event an added event. # def added? type == :added end # Is the event a removed event. # def removed? type == :removed end # Is the event a stable event. # def stable? type == :stable end # Convert the Event to a nice string format # def to_s( ) "<#{self.class} type: #{type} path: '#{path}'>" end end directory_watcher-1.5.1/lib/directory_watcher/eventable_scanner.rb0000644000004100000410000001457212127515724025600 0ustar www-datawww-data# An Eventable Scanner is one that can be utilized by something that has an # Event Loop. It is intended to be subclassed by classes that implement the # specific event loop semantics for say EventMachine or Cool.io. # # The Events that the EventableScanner is programmed for are: # # on_scan - this should be called every +interval+ times # on_modified - If the event loop can monitor individual files then this should # be called when the file is modified # on_removed - Similar to on_modified but called when a file is removed. # # Sub classes are required to implement the following: # # start_loop_with_attached_scan_timer() - Instance Method # This method is to start up the loop, if necessary assign to @loop_thread # instance variable the Thread that is controlling the event loop. # # This method must also assign an object to @timer which is what does the # periodic scanning of the globs. This object must respond to +detach()+ so # that it may be detached from the event loop. # # stop_loop() - Instance Method # This method must shut down the event loop, or detach these classes from # the event loop if we just attached to an existing event loop. # # Watcher - An Embedded class # This is a class that must have a class method +watcher(path,scanner)+ # which is used to instantiate a file watcher. The Watcher instance must # respond to +detach()+ so that it may be independently detached from the # event loop. # class DirectoryWatcher::EventableScanner include DirectoryWatcher::Logable # A Hash of Watcher objects. attr_reader :watchers # call-seq: # EventableScanner.new( config ) # # config - the Configuration instances # def initialize( config ) @config = config @scan_and_queue = DirectoryWatcher::ScanAndQueue.new(config.glob, config.collection_queue) @watchers = {} @stopping = false @timer = nil @loop_thread = nil @paused = false end # The queue on which to put FileStat and Scan items. # def collection_queue @config.collection_queue end # The interval at which to scan # def interval @config.interval end # Returns +true+ if the scanner is currently running. Returns +false+ if # this is not the case. # def running? return !@stopping if @timer return false end # Start up the scanner. If the scanner is already running, nothing happens. # def start return if running? logger.debug "starting scanner" start_loop_with_attached_scan_timer end # Stop the scanner. If the scanner is not running, nothing happens. # def stop return unless running? logger.debug "stoping scanner" @stopping = true teardown_timer_and_watches @stopping = false stop_loop end # Pause the scanner. # # Pausing the scanner does not stop the scanning per se, it stops items from # being sent to the collection queue # def pause logger.debug "pausing scanner" @paused = true end # Resume the scanner. # # This removes the blockage on sending items to the collection queue. # def resume logger.debug "resuming scanner" @paused = false end # Is the Scanner currently paused. # def paused? @paused end # EventableScanners do not join # def join( limit = nil ) end # Do a single scan and send those items to the collection queue. # def run logger.debug "running scan and queue" @scan_and_queue.scan_and_queue end # Setting maximum iterations means hooking into the periodic timer event and # counting the number of times it is going on. This also resets the current # iterations count # def maximum_iterations=(value) unless value.nil? value = Integer(value) raise ArgumentError, "maximum iterations must be >= 1" unless value >= 1 end @iterations = 0 @maximum_iterations = value end attr_reader :maximum_iterations attr_reader :iterations # Have we completed up to the maximum_iterations? # def finished_iterations? self.iterations >= self.maximum_iterations end # This callback is invoked by the Timer instance when it is triggered by # the Loop. This method will check for added files and stable files # and notify the directory watcher accordingly. # def on_scan logger.debug "on_scan called" scan_and_watch_files progress_towards_maximum_iterations end # This callback is invoked by the Watcher instance when it is triggered by the # loop for file modifications. # def on_modified(watcher, new_stat) logger.debug "on_modified called" queue_item(new_stat) end # This callback is invoked by the Watcher instance when it is triggered by the # loop for file removals # def on_removed(watcher, new_stat) logger.debug "on_removed called" unwatch_file(watcher.path) queue_item(new_stat) end ####### private ####### # Send the given item to the collection queue # def queue_item( item ) if paused? then logger.debug "Not queueing item, we're paused" else logger.debug "enqueuing #{item} to #{collection_queue}" collection_queue.enq item end end # Run a single scan and turn on watches for all the files found in that scan # that do not already have watchers on them. # def scan_and_watch_files logger.debug "scanning and watching files" scan = @scan_and_queue.scan_and_queue scan.results.each do |stat| watch_file(stat.path) end end # Remove the timer and the watches from the event loop # def teardown_timer_and_watches @timer.detach rescue nil @timer = nil @watchers.each_value {|w| w.detach} @watchers.clear end # Create and return a new Watcher instance for the given filename _fn_. # A watcher will only be created once for a particular fn. # def watch_file( fn ) unless @watchers[fn] then logger.debug "Watching file #{fn}" w = self.class::Watcher.watch(fn, self) @watchers[fn] = w end end # Remove the watcher instance from our tracking # def unwatch_file( fn ) logger.debug "Unwatching file #{fn}" @watchers.delete(fn) end # Make progress towards maximum iterations. And if we get there, then stop # monitoring files. # def progress_towards_maximum_iterations if maximum_iterations then @iterations += 1 stop if finished_iterations? end end # :startdoc: end # class DirectoryWatcher::Eventablecanner directory_watcher-1.5.1/lib/directory_watcher/rev_scanner.rb0000644000004100000410000000703612127515724024424 0ustar www-datawww-databegin require 'rev' DirectoryWatcher::HAVE_REV = true rescue LoadError DirectoryWatcher::HAVE_REV = false end if DirectoryWatcher::HAVE_REV # Deprecated: # # The RevScanner uses the Rev loop to monitor changes to files in the # watched directory. This scanner is more efficient than the pure Ruby # scanner because it relies on the operating system kernel notifications # instead of a periodic polling and stat of every file in the watched # directory (the technique used by the Scanner class). # # The RevScanner is essentially the exact same as the CoolioScanner with class # names changed and using _rev_loop instead of _coolio_loop. Unfortunately the # RevScanner cannot be a sub class of CoolioScanner because of C-extension # reasons between the rev and coolio gems # # Rev cannot notify us when a file is added to the watched # directory; therefore, added files are only picked up when we apply the # glob pattern to the directory. This is done at the configured interval. # class DirectoryWatcher::RevScanner < ::DirectoryWatcher::EventableScanner # call-seq: # RevScanner.new( glob, interval, collection_queue ) # def initialize( glob, interval, collection_queue ) super(glob, interval, collection_queue) end # Called by EventablScanner#start to start the loop up and attach the periodic # timer that will poll the globs for new files. # def start_loop_with_attached_scan_timer return if @loop_thread @timer = ScanTimer.new( self ) @loop_thread = Thread.new { @timer.attach(event_loop) event_loop.run } end # Called by EventableScanner#stop to stop the loop as part of the shutdown # process. # def stop_loop if @loop_thread then event_loop.stop rescue nil @loop_thread.kill @loop_thread = nil end end # Return the rev loop object # # This is used during the startup, shutdown process and for the Watcher to # attach and detach from the event loop # def event_loop if @loop_thread then @loop_thread._rev_loop else Thread.current._rev_loop end end # :stopdoc: # # Watch files using the Rev::StatWatcher. # # The rev +on_change+ callback is converted to the appropriate +on_removed+ # and +on_modified+ callbacks for the EventableScanner. class Watcher < ::Rev::StatWatcher def self.watch(fn, scanner ) new(fn, scanner) end def initialize( fn, scanner ) # for file watching, we want to make sure this happens at a reasonable # value, so set it to 0 if the scanner.interval is > 5 seconds. This will # make it use the system value, and allow us to test. i = scanner.interval < 5 ? scanner.interval : 0 super(fn, i) @scanner = scanner attach(scanner.event_loop) end # Rev uses on_change so we convert that to the appropriate # EventableScanner calls. Unlike Coolio, Rev's on_change() takes no # parameters # def on_change if File.exist?(path) then @scanner.on_removed(self, ::DirectoryWatcher::FileStat.for_removed_path(path)) else stat = File.stat(path) @scanner.on_modified(self, ::DirectoryWatcher::FileStat.new(path, stat.mtime, stat.size)) end end end # Periodically execute a Scan. Hook this into the EventableScanner#on_scan # class ScanTimer< Rev::TimerWatcher def initialize( scanner ) super(scanner.interval, true) @scanner = scanner end def on_timer( *args ) @scanner.on_scan end end end # class DirectoryWatcher::RevScanner end # if DirectoryWatcher::HAVE_REV directory_watcher-1.5.1/lib/directory_watcher/paths.rb0000644000004100000410000000276312127515724023240 0ustar www-datawww-dataclass DirectoryWatcher # Paths contains helpful methods to determine paths of files inside the # DirectoryWatcher library # module Paths # The root directory of the project is considered the parent directory of # the 'lib' directory. # # Returns The full expanded path of the parent directory of 'lib' going up # the path from the current file. Trailing File::SEPARATOR is guaranteed # def root_dir path_parts = ::File.expand_path(__FILE__).split(::File::SEPARATOR) lib_index = path_parts.rindex("lib") return path_parts[0...lib_index].join(::File::SEPARATOR) + ::File::SEPARATOR end # Return a path relative to the 'lib' directory in this project # def lib_path(*args,&block) sub_path('lib', *args, &block) end # Return a path relative to the 'root' directory in the project # def path(*args,&block) sub_path('', *args, &block) end # Calculate the full expanded path of the item with respect to a sub path of # 'root_dir' # def sub_path(sub,*args,&block) rv = ::File.join(root_dir, sub) + ::File::SEPARATOR rv = ::File.join(rv, *args) if args if block with_load_path( rv ) do rv = block.call end end return rv end # Execute a block in the context of a path added to $LOAD_PATH # def with_load_path(path, &block) $LOAD_PATH.unshift path block.call ensure $LOAD_PATH.shift end extend self end end directory_watcher-1.5.1/lib/directory_watcher/scanner.rb0000644000004100000410000000316012127515724023542 0ustar www-datawww-data# The Scanner is responsible for polling the watched directory at a regular # interval and generating a Scan which it will then send down the collection # queue to the Collector. # # The Scanner is a pure Ruby class, and as such it works across all Ruby # interpreters on the major platforms. This also means that it can be # processor intensive for large numbers of files or very fast update # intervals. Your mileage will vary, but it is something to keep an eye on. # class DirectoryWatcher::Scanner include DirectoryWatcher::Threaded include DirectoryWatcher::Logable # call-seq: # Scanner.new( configuration ) # # From the Configuration instance passed in Scanner uses: # # glob - Same as that in DirectoryWatcher # interval - Same as that in DirectoryWatcher # collection_queue - The Queue to send the Scans too. # the other end of this queue is connected to a Collector # # The Scanner is not generally used out side of a DirectoryWatcher so this is # more of an internal API # #def initialize( glob, interval, collection_queue ) def initialize( config ) @config = config @scan_and_queue = ::DirectoryWatcher::ScanAndQueue.new( @config.glob, @config.collection_queue ) end # Set the interval before starting the loop. # This allows for interval to be set AFTER the DirectoryWatcher instance is # allocated but before it is started. def before_starting self.interval = @config.interval end # Performs exactly one scan of the directory and sends the # results to the Collector # def run @scan_and_queue.scan_and_queue end end directory_watcher-1.5.1/lib/directory_watcher/notifier.rb0000644000004100000410000000261412127515724023733 0ustar www-datawww-data# A Notifier pull Event instances from the give queue and sends them to all of # the Observers it knows about. # class DirectoryWatcher::Notifier include DirectoryWatcher::Threaded include DirectoryWatcher::Logable # Create a new Notifier that pulls events off the given notification_queue from the # config, and sends them to the listed observers. # def initialize( config, observers ) @config = config @observers = observers self.interval = 0.01 # yes this is a fast loop end # Notify all the observers of all the available events in the queue. # If there are 2 or more events in a row that are the same, then they are # collapsed into a single event. # def run previous_event = nil until queue.empty? do event = queue.deq next if previous_event == event @observers.each do |observer, func| send_event_to_observer( observer, func, event ) end previous_event = event end end ####### private ####### def queue @config.notification_queue end # Send the given event to the given observer using the given function. # # Capture any exceptions that have, swallow them and send them to stderr. def send_event_to_observer( observer, func, event ) observer.send(func, event) rescue Exception => e $stderr.puts "Called #{observer}##{func}(#{event}) and all I got was this lousy exception #{e}" end end directory_watcher-1.5.1/lib/directory_watcher/threaded.rb0000644000004100000410000002074112127515724023675 0ustar www-datawww-data# # == Synopsis # The Threaded module is used to perform some activity at a specified # interval. # # == Details # Sometimes it is useful for an object to have its own thread of execution # to perform a task at a recurring interval. The Threaded module # encapsulates this functionality so you don't have to write it yourself. It # can be used with any object that responds to the +run+ method. # # The threaded object is run by calling the +start+ method. This will create # a new thread that will invoke the +run+ method at the desired interval. # Just before the thread is created the +before_starting+ method will be # called (if it is defined by the threaded object). Likewise, after the # thread is created the +after_starting+ method will be called (if it is # defined by the threaded object). # # The threaded object is stopped by calling the +stop+ method. This sets an # internal flag and then wakes up the thread. The thread gracefully exits # after checking the flag. Like the start method, before and after methods # are defined for stopping as well. Just before the thread is stopped the # +before_stopping+ method will be called (if it is defined by the threaded # object). Likewise, after the thread has died the +after_stopping+ method # will be called (if it is defined by the threaded object). # # Calling the +join+ method on a threaded object will cause the calling # thread to wait until the threaded object has stopped. An optional timeout # parameter can be given. # module DirectoryWatcher::Threaded # This method will be called by the activity thread at the desired # interval. Implementing classes are expect to provide this # functionality. # def run raise NotImplementedError, 'The run method must be defined by the threaded object.' end # Start the activity thread. If already started this method will return # without taking any action. # # If the including class defines a 'before_starting' method, it will be # called before the thread is created and run. Likewise, if the # including class defines an 'after_starting' method, it will be called # after the thread is created. # def start return self if _activity_thread.running? before_starting if self.respond_to?(:before_starting) @_activity_thread.start self after_starting if self.respond_to?(:after_starting) self end # Stop the activity thread. If already stopped this method will return # without taking any action. # # If the including class defines a 'before_stopping' method, it will be # called before the thread is stopped. Likewise, if the including class # defines an 'after_stopping' method, it will be called after the thread # has stopped. # def stop return self unless _activity_thread.running? before_stopping if self.respond_to?(:before_stopping) @_activity_thread.stop self end # Stop the activity thread from doing work. This will not stop the activity # thread, it will just stop it from calling the 'run' method on every # iteration. It will also not increment the number of iterations it has run. def pause @_activity_thread.working = false end # Resume the activity thread def resume @_activity_thread.working = true end # Wait on the activity thread. If the thread is already stopped, this # method will return without taking any action. Otherwise, this method # does not return until the activity thread has stopped, or a specific # number of iterations has passed since this method was called. # def wait( limit = nil ) return self unless _activity_thread.running? initial_iterations = @_activity_thread.iterations loop { break unless @_activity_thread.running? break if limit and @_activity_thread.iterations > ( initial_iterations + limit ) Thread.pass } end # If the activity thread is running, the calling thread will suspend # execution and run the activity thread. This method does not return until # the activity thread is stopped or until _limit_ seconds have passed. # # If the activity thread is not running, this method returns immediately # with +nil+. # def join( limit = nil ) _activity_thread.join(limit) ? self : nil end # Returns +true+ if the activity thread is running. Returns +false+ # otherwise. # def running? _activity_thread.running? end # Returns +true+ if the activity thread has finished its maximum # number of iterations or the thread is no longer running. # Returns +false+ otherwise. # def finished_iterations? return true unless _activity_thread.running? @_activity_thread.finished_iterations? end # Returns the status of threaded object. # # 'sleep' : sleeping or waiting on I/O # 'run' : executing # 'aborting' : aborting # false : not running or terminated normally # nil : terminated with an exception # # If this method returns +nil+, then calling join on the threaded object # will cause the exception to be raised in the calling thread. # def status return false if _activity_thread.thread.nil? @_activity_thread.thread.status end # Sets the number of seconds to sleep between invocations of the # threaded object's 'run' method. # def interval=( value ) value = Float(value) raise ArgumentError, "Sleep interval must be >= 0" unless value >= 0 _activity_thread.interval = value end # Returns the number of seconds to sleep between invocations of the # threaded object's 'run' method. # def interval _activity_thread.interval end # Sets the maximum number of invocations of the threaded object's # 'run' method # def maximum_iterations=( value ) unless value.nil? value = Integer(value) raise ArgumentError, "maximum iterations must be >= 1" unless value >= 1 end _activity_thread.maximum_iterations = value end # Returns the maximum number of invocations of the threaded # object's 'run' method # def maximum_iterations _activity_thread.maximum_iterations end # Returns the number of iterations of the threaded object's 'run' method # completed thus far. # def iterations _activity_thread.iterations end # Set to +true+ to continue running the threaded object even if an error # is raised by the +run+ method. The default behavior is to stop the # activity thread when an error is raised by the run method. # # A SystemExit will never be caught; it will always cause the Ruby # interpreter to exit. # def continue_on_error=( value ) _activity_thread.continue_on_error = (value ? true : false) end # Returns +true+ if the threaded object should continue running even if an # error is raised by the run method. The default is to return +false+. The # threaded object will stop running when an error is raised. # def continue_on_error? _activity_thread.continue_on_error end # :stopdoc: def _activity_thread @_activity_thread ||= ::DirectoryWatcher::Threaded::ThreadContainer.new(60, 0, nil, false); end # @private # @private ThreadContainer = Struct.new( :interval, :iterations, :maximum_iterations, :continue_on_error, :thread, :running, :working) { def start( threaded ) self.working = true self.running = true self.iterations = 0 self.thread = Thread.new { run threaded } Thread.pass end # @private def stop self.running = false thread.wakeup end # @private def run( threaded ) loop do begin break unless running? do_work( threaded ) sleep interval if running? rescue SystemExit; raise rescue Exception => err if continue_on_error $stderr.puts err else $stderr.puts err raise err end end end ensure if threaded.respond_to?(:after_stopping) and !self.running threaded.after_stopping end self.running = false end # @private def join( limit = nil ) return if thread.nil? limit ? thread.join(limit) : thread.join end # @private def do_work( threaded ) if working then threaded.run if maximum_iterations self.iterations += 1 if finished_iterations? self.running = false end end end end # @private def finished_iterations? return true if maximum_iterations and (iterations >= maximum_iterations) return false end # @private alias :running? :running } # :startdoc: end directory_watcher-1.5.1/lib/directory_watcher/scan.rb0000644000004100000410000000321612127515724023037 0ustar www-datawww-data# A Scan is the scan of a full directory structure with the ability to iterate # over the results, or return them as a full dataset # # results = Scan.new( globs ).run # class DirectoryWatcher::Scan def initialize( globs = Array.new ) @globs = [ globs ].flatten @results = Array.new end # Run the entire scan and collect all the results. The Scan will only ever # be run once. # # Return the array of FileStat results def run results end # Return the results of the scan. If the scan has not been run yet, then run # it def results @results = collect_all_stats if @results.empty? return @results end ####### private ####### # Collect all the Stats into an Array and return them # def collect_all_stats r = [] each { |stat| r << stat } return r end # Iterate over each glob, yielding it # def each_glob( &block ) @globs.each do |glob| yield glob end end # Iterate over each item that matches the glob. # The item yielded is a ::DirectoryWatcher::FileStat object. # def each( &block ) each_glob do |glob| Dir.glob(glob).each do |fn| if stat = file_stat( fn ) then yield stat if block_given? end end end end # Return the stat of of the file in question. If the item is not a file, # then return the value of the passed in +if_not_file+ # def file_stat( fn, if_not_file = false ) stat = File.stat fn return if_not_file unless stat.file? return DirectoryWatcher::FileStat.new( fn, stat.mtime, stat.size ) rescue SystemCallError => e # swallow $stderr.puts "Error Stating #{fn} : #{e}" end end directory_watcher-1.5.1/lib/directory_watcher/logable.rb0000644000004100000410000000103112127515724023511 0ustar www-datawww-dataclass DirectoryWatcher # This is the implementation of a logger that does nothing. # It has all the debug, info, warn, error, fatal methods, but they do nothing class NullLogger def debug( msg ); end def info( msg ); end def warn( msg ); end def error( msg ); end def fatal( msg ); end end module Logable def logger @config.logger end def self.default_logger require 'logging' Logging::Logger[DirectoryWatcher] rescue LoadError NullLogger.new end end end directory_watcher-1.5.1/lib/directory_watcher/scan_and_queue.rb0000644000004100000410000000100112127515724025053 0ustar www-datawww-data# ScanAndQueue creates a Scan from its input globs and then sends that Scan to # its Queue. # # Every time scan_and_queue is called a new scan is created an sent to the # queue. class DirectoryWatcher::ScanAndQueue def initialize( glob, queue ) @globs = glob @queue =queue end # Create and run a Scan and submit it to the Queue. # # Returns the Scan that was run def scan_and_queue scan = ::DirectoryWatcher::Scan.new( @globs ) scan.run @queue.enq scan return scan end end directory_watcher-1.5.1/lib/directory_watcher/collector.rb0000644000004100000410000002113012127515724024074 0ustar www-datawww-data# Collector reads items from a collection Queue and processes them to see if # FileEvents should be put onto the notification Queue. # class DirectoryWatcher::Collector include DirectoryWatcher::Threaded include DirectoryWatcher::Logable # Create a new StatCollector from the given Configuration, and an optional # Scan. # # configuration - The Collector uses from Configuration: # collection_queue - The Queue to read items from the Scanner on # notification_queue - The Queue to submit the Events to the Notifier on # stable - The number of times we see a file hasn't changed before # emitting a stable event # sort_by - the method used to sort events during on_scan results # order_by - The method used to order events from call to on_scan # # pre_load_scan - A Scan to use to load our internal state from before. No # events will be emitted for the FileStat's in this scan. # #def initialize( notification_queue, collection_queue, options = {} ) def initialize( config ) @stats = Hash.new @stable_counts = Hash.new @config = config on_scan( DirectoryWatcher::Scan.new( config.glob ), false ) if config.pre_load? self.interval = 0.01 # yes this is a fast loop end # The number of times we see a file hasn't changed before emitting a stable # count. See Configuration#stable def stable_threshold @config.stable end # How to sort Scan results. See Configuration. # def sort_by @config.sort_by end # How to order Scan results. See Configuration. # def order_by @config.order_by end # The queue from which to read items from the scanners. See Configuration. # def collection_queue @config.collection_queue end # The queue to write Events for the Notifier. See Configuration. # def notification_queue @config.notification_queue end # Given the scan, update the set of stats with the results from the Scan and # emit events to the notification queue as appropriate. # # scan - The Scan containing all the new FileStat items # emit_events - Should events be emitted for the events in the scan # (default: true) # # There is one odd thing that happens here. Scanners that are EventableScanners # use on_stat to emit removed events, and the standard threaded Scanner only # uses Scans. So we make sure and only emit removed events in this method if # the scanner that gave us the scan was the basic threaded Scanner. # # TODO: Possibly fix this through another abstraction in the Scanners. # No idea about what that would be yet. # # Returns nothing. # def on_scan( scan, emit_events = true ) seen_paths = Set.new logger.debug "Sorting by #{sort_by} #{order_by}" sorted_stats( scan.run ).each do |stat| on_stat(stat, emit_events) seen_paths << stat.path end emit_removed_events(seen_paths) if @config.scanner.nil? end # Process a single stat and emit an event if necessary. # # stat - The new FileStat to process and see if an event should # be emitted # emit_event - Whether or not an event should be emitted. # # Returns nothing def on_stat( stat, emit_event = true ) orig_stat = update_stat( stat ) logger.debug "Emitting event for on_stat #{stat}" emit_event_for( orig_stat, stat ) if emit_event end # Remove one item from the collection queue and process it. # # This method is required by the Threaded API # # Returns nothing def run case thing = collection_queue.deq when ::DirectoryWatcher::Scan on_scan(thing) when ::DirectoryWatcher::FileStat on_stat(thing) else raise "Unknown item in the queue: #{thing}" end end # Write the current stats to the given IO object as a YAML document. # # io - The IO object to write the document to. # # Returns nothing. def dump_stats( io ) YAML.dump(@stats, io) end # Read the current stats from the given IO object. Any existing stats in the # Collector will be overwritten # # io - The IO object from which to read the document. # # Returns nothing. def load_stats( io ) @stats = YAML.load(io) end ####### private ####### # Sort the stats by +sort_by+ and +order_by+ returning the results # def sorted_stats( stats ) sorted = stats.sort_by{ |stat| stat.send(sort_by) } sorted = sorted.reverse if order_by == :descending return sorted end # Update the stats Hash with the new_stat information, return the old data # that is being replaced. # def update_stat( new_stat ) old_stat = @stats.delete(new_stat.path) @stats.store(new_stat.path, new_stat) unless new_stat.removed? return old_stat end # Look for removed files and emit removed events for all of them. # # seen_paths - the list of files that we know currently exist # # Return nothing def emit_removed_events( seen_paths ) @stats.keys.each do |existing_path| next if seen_paths.include?(existing_path) old_stat = @stats.delete(existing_path) emit_event_for(old_stat, ::DirectoryWatcher::FileStat.for_removed_path(existing_path)) end end # Determine what type of event to emit, and put that event onto the # notification queue. # # old_stat - The old FileStat # new_stat - The new FileStat # # Returns nothing def emit_event_for( old_stat, new_stat ) event = DirectoryWatcher::Event.from_stats( old_stat, new_stat ) if should_emit?(event) then logger.debug "Sending event #{event.object_id} to notifcation queue" notification_queue.enq( event ) else logger.debug "Emitting of event #{event.object_id} cancelled" end end # Should the event given actually be emitted. # # If the event passed in is NOT a stable event, return true # If there is a stable_threshold, then check to see if the stable count for # this event's path has crossed the stable threshold. # # This method has the side effect of updating the stable count of the path of # the event. If we are going to return true for the stable event, then we # reset the stable count of that event to 0. # # event - any event # # Returns whether or not to emit the event based upon its stability def should_emit?( event ) if event.stable? then if emitting_stable_events? and valid_for_stable_event?( event.path )then increment_stable_count( event.path ) if should_emit_stable?( event.path ) then mark_as_invalid_for_stable_event( event.path ) return true end end return false elsif event.removed? then mark_as_invalid_for_stable_event( event.path ) return true else mark_as_valid_for_stable_event( event.path ) return true end end # Is the given path able to have a stable event emitted for it? # # A stable event may only be emitted for a path that has already had an added # or modified event already sent. Also, once a stable event has been emitted # for a path, another stable event may not be emitted until it has been # modified, or added again. # # path - the path of the file to check # # Returns whether or not the path may have a stable event emitted for it. def valid_for_stable_event?( path ) @stable_counts.has_key?( path ) end # Let it be known that the given path can now have a stable event emitted for # it. # # path - the path to mark as ready # # Returns nothing def mark_as_valid_for_stable_event( path ) logger.debug "#{path} marked as valid for stable" @stable_counts[path] = 0 end # Mark that the given path is invalid for having a stable event emitted for # it. # # path - the path to mark # # Returns nothing def mark_as_invalid_for_stable_event( path ) logger.debug "#{path} marked as invalid for stable" @stable_counts.delete( path ) end # Increment the stable count for the given path # # path - the path of the file to increment its stable count # # Returns nothing def increment_stable_count( path ) @stable_counts[path] += 1 end # Is the given path ready to have a stable event emitted? # # path - the path to report on # # Returns whether to emit a stable event or not def should_emit_stable?( path ) @stable_counts[path] >= stable_threshold end # Is it legal for us to emit stable events at all. This checks the config to # see if that is the case. # # In the @config if the stable threshold is set then we are emitting stable # events. # # Returns whether it is legal to propogate stable events def emitting_stable_events? stable_threshold end end