# Phusion Passenger - https://www.phusionpassenger.com/ # Copyright (c) 2010 Phusion # # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui. # # 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. PhusionPassenger.require_passenger_lib 'native_support' module PhusionPassenger module Utils # Watches changes on one or more files or directories. To use this class, # construct an object, passing it file or directory names to watch, then # call #wait_for_change. #wait_for_change waits until one of the following # events has happened since the constructor was called: # # - One of the specified files has been renamed, deleted, or its access # revoked. This will cause +true+ to be returned. # - One of the specified directories has been modified, renamed, deleted, # or its access revoked. This will cause +true+ to be returned. # - +termination_pipe+ (as passed to the constructor) becomes readable. # This will cause +nil+ to be returned. # - The thread is interrupted. This will cause +nil+ to be returned. # # The constructor will attempt to stat and possibly also open all specified # files/directories. If one of them cannot be statted or opened, then # +false+ will be returned by #wait_for_change. # # #wait_for_change may only be called once. After calling it one should # create a new object if one wishes to watch the filesystem again. # # Always call #close when a FileSystemWatcher object is no longer needed # in order to free resources. # # This class tries to use kqueue for efficient filesystem watching on # platforms that support it. On other platforms it'll fallback to stat # polling instead. if defined?(NativeSupport::FileSystemWatcher) FileSystemWatcher = NativeSupport::FileSystemWatcher FileSystemWatcher.class_eval do def self.new(filenames, termination_pipe = nil) # Default parameter values, type conversion and exception # handling in C is too much of a pain. filenames = filenames.map do |filename| filename.to_s end return _new(filenames, termination_pipe) end def self.opens_files? return true end end else class FileSystemWatcher attr_accessor :poll_interval def self.opens_files? return false end def initialize(filenames, termination_pipe = nil) @poll_interval = 3 @termination_pipe = termination_pipe @dirs = [] @files = [] begin filenames.each do |filename| stat = File.stat(filename) if stat.directory? @dirs << DirInfo.new(filename, stat) else @files << FileInfo.new(filename, stat) end end rescue Errno::EACCES, Errno::ENOENT @dirs = @files = nil end end def wait_for_change if !@dirs return false end while true if changed? return true elsif select([@termination_pipe], nil, nil, @poll_interval) return nil end end end def close end private class DirInfo DOT = "." DOTDOT = ".." def initialize(filename, stat) @filename = filename @stat = stat @subfiles = {} Dir.foreach(filename) do |entry| next if entry == DOT || entry == DOTDOT subfilename = "#{filename}/#{entry}" @subfiles[entry] = FileInfo.new(subfilename, File.stat(subfilename)) end end def changed? new_stat = File.stat(@filename) if @stat.ino != new_stat.ino || !new_stat.directory? || @stat.mtime != new_stat.mtime return true end count = 0 Dir.foreach(@filename) do |entry| next if entry == DOT || entry == DOTDOT subfilename = "#{@filename}/#{entry}" file_info = @subfiles[entry] if !file_info || file_info.changed?(false) return true else count += 1 end end return count != @subfiles.size rescue Errno::EACCES, Errno::ENOENT return true end end class FileInfo def initialize(filename, stat) @filename = filename @stat = stat end def changed?(check_mtime = true) new_stat = File.stat(@filename) if check_mtime mtime_changed = @stat.mtime != new_stat.mtime || @stat.size != new_stat.size else mtime_changed = false end return @stat.ino != new_stat.ino || @stat.ftype != new_stat.ftype || mtime_changed rescue Errno::EACCES, Errno::ENOENT return true end end def changed? return @dirs.any? { |dir_info| dir_info.changed? } || @files.any? { |file_info| file_info.changed? } end end end end # module Utils end # module PhusionPassenger