├── VERSION ├── .gitignore ├── .yardopts ├── lib ├── rb-inotify.rb └── rb-inotify │ ├── native.rb │ ├── native │ └── flags.rb │ ├── watcher.rb │ ├── event.rb │ └── notifier.rb ├── MIT-LICENSE ├── rb-inotify.gemspec ├── Rakefile └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yardoc 2 | /doc 3 | /pkg 4 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --readme README.md 2 | --markup markdown 3 | --markup-provider maruku 4 | --no-private 5 | -------------------------------------------------------------------------------- /lib/rb-inotify.rb: -------------------------------------------------------------------------------- 1 | require 'rb-inotify/native' 2 | require 'rb-inotify/native/flags' 3 | require 'rb-inotify/notifier' 4 | require 'rb-inotify/watcher' 5 | require 'rb-inotify/event' 6 | 7 | # The root module of the library, which is laid out as so: 8 | # 9 | # * {Notifier} -- The main class, where the notifications are set up 10 | # * {Watcher} -- A watcher for a single file or directory 11 | # * {Event} -- An filesystem event notification 12 | module INotify 13 | # An array containing the version number of rb-inotify. 14 | # The numbers in the array are the major, minor, and patch versions, 15 | # respectively. 16 | VERSION = [0, 9, 0] 17 | end 18 | -------------------------------------------------------------------------------- /lib/rb-inotify/native.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | module INotify 4 | # This module contains the low-level foreign-function interface code 5 | # for dealing with the inotify C APIs. 6 | # It's an implementation detail, and not meant for users to deal with. 7 | # 8 | # @private 9 | module Native 10 | extend FFI::Library 11 | ffi_lib FFI::Library::LIBC 12 | 13 | # The C struct describing an inotify event. 14 | # 15 | # @private 16 | class Event < FFI::Struct 17 | layout( 18 | :wd, :int, 19 | :mask, :uint32, 20 | :cookie, :uint32, 21 | :len, :uint32) 22 | end 23 | 24 | attach_function :inotify_init, [], :int 25 | attach_function :inotify_add_watch, [:int, :string, :uint32], :int 26 | attach_function :inotify_rm_watch, [:int, :uint32], :int 27 | 28 | attach_function :read, [:int, :pointer, :size_t], :ssize_t 29 | attach_function :close, [:int], :int 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Nathan Weizenbaum 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /rb-inotify.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "rb-inotify" 8 | s.version = "0.9.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Nathan Weizenbaum"] 12 | s.date = "2013-01-12" 13 | s.description = "A Ruby wrapper for Linux's inotify, using FFI" 14 | s.email = "nex342@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".yardopts", 20 | "MIT-LICENSE", 21 | "README.md", 22 | "Rakefile", 23 | "VERSION", 24 | "lib/rb-inotify.rb", 25 | "lib/rb-inotify/event.rb", 26 | "lib/rb-inotify/native.rb", 27 | "lib/rb-inotify/native/flags.rb", 28 | "lib/rb-inotify/notifier.rb", 29 | "lib/rb-inotify/watcher.rb", 30 | "rb-inotify.gemspec" 31 | ] 32 | s.homepage = "http://github.com/nex3/rb-inotify" 33 | s.require_paths = ["lib"] 34 | s.rubygems_version = "1.8.24" 35 | s.summary = "A Ruby wrapper for Linux's inotify, using FFI" 36 | 37 | if s.respond_to? :specification_version then 38 | s.specification_version = 3 39 | 40 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 41 | s.add_runtime_dependency(%q, [">= 0.5.0"]) 42 | s.add_development_dependency(%q, [">= 0.4.0"]) 43 | else 44 | s.add_dependency(%q, [">= 0.5.0"]) 45 | s.add_dependency(%q, [">= 0.4.0"]) 46 | end 47 | else 48 | s.add_dependency(%q, [">= 0.5.0"]) 49 | s.add_dependency(%q, [">= 0.4.0"]) 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "rb-inotify" 8 | gem.summary = "A Ruby wrapper for Linux's inotify, using FFI" 9 | gem.description = gem.summary 10 | gem.email = "nex342@gmail.com" 11 | gem.homepage = "http://github.com/nex3/rb-inotify" 12 | gem.authors = ["Nathan Weizenbaum"] 13 | gem.add_dependency "ffi", ">= 0.5.0" 14 | gem.add_development_dependency "yard", ">= 0.4.0" 15 | end 16 | Jeweler::GemcutterTasks.new 17 | rescue LoadError 18 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 19 | end 20 | 21 | task(:permissions) {sh %{chmod -R a+r .}} 22 | Rake::Task[:build].prerequisites.unshift('permissions') 23 | 24 | module Jeweler::VersionHelper::PlaintextExtension 25 | def write_with_inotify 26 | write_without_inotify 27 | filename = File.join(File.dirname(__FILE__), "lib/rb-inotify.rb") 28 | text = File.read(filename) 29 | File.open(filename, 'w') do |f| 30 | f.write text.gsub(/^( VERSION = ).*/, '\1' + [major, minor, patch].inspect) 31 | end 32 | end 33 | alias_method :write_without_inotify, :write 34 | alias_method :write, :write_with_inotify 35 | end 36 | 37 | class Jeweler::Commands::Version::Base 38 | def commit_version_with_inotify 39 | return unless self.repo 40 | self.repo.add(File.join(File.dirname(__FILE__), "lib/rb-inotify.rb")) 41 | commit_version_without_inotify 42 | end 43 | alias_method :commit_version_without_inotify, :commit_version 44 | alias_method :commit_version, :commit_version_with_inotify 45 | end 46 | 47 | begin 48 | require 'yard' 49 | YARD::Rake::YardocTask.new 50 | rescue LoadError 51 | task :yardoc do 52 | abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rb-inotify 2 | 3 | This is a simple wrapper over the [inotify](http://en.wikipedia.org/wiki/Inotify) Linux kernel subsystem 4 | for monitoring changes to files and directories. 5 | It uses the [FFI](http://wiki.github.com/ffi/ffi) gem to avoid having to compile a C extension. 6 | 7 | [API documentation is available on rdoc.info](http://rdoc.info/projects/nex3/rb-inotify). 8 | 9 | ## Basic Usage 10 | 11 | The API is similar to the inotify C API, but with a more Rubyish feel. 12 | First, create a notifier: 13 | 14 | notifier = INotify::Notifier.new 15 | 16 | Then, tell it to watch the paths you're interested in 17 | for the events you care about: 18 | 19 | notifier.watch("path/to/foo.txt", :modify) {puts "foo.txt was modified!"} 20 | notifier.watch("path/to/bar", :moved_to, :create) do |event| 21 | puts "#{event.name} is now in path/to/bar!" 22 | end 23 | 24 | Inotify can watch directories or individual files. 25 | It can pay attention to all sorts of events; 26 | for a full list, see [the inotify man page](http://www.tin.org/bin/man.cgi?section=7&topic=inotify). 27 | 28 | Finally, you get at the events themselves: 29 | 30 | notifier.run 31 | 32 | This will loop infinitely, calling the appropriate callbacks when the files are changed. 33 | If you don't want infinite looping, 34 | you can also block until there are available events, 35 | process them all at once, 36 | and then continue on your merry way: 37 | 38 | notifier.process 39 | 40 | ## Advanced Usage 41 | 42 | Sometimes it's necessary to have finer control over the underlying IO operations 43 | than is provided by the simple callback API. 44 | The trick to this is that the \{INotify::Notifier#to_io Notifier#to_io} method 45 | returns a fully-functional IO object, 46 | with a file descriptor and everything. 47 | This means, for example, that it can be passed to `IO#select`: 48 | 49 | # Wait 10 seconds for an event then give up 50 | if IO.select([notifier.to_io], [], [], 10) 51 | notifier.process 52 | end 53 | 54 | It can even be used with EventMachine: 55 | 56 | require 'eventmachine' 57 | 58 | EM.run do 59 | EM.watch notifier.to_io do 60 | notifier.process 61 | end 62 | end 63 | 64 | Unfortunately, this currently doesn't work under JRuby. 65 | JRuby currently doesn't use native file descriptors for the IO object, 66 | so we can't use the notifier's file descriptor as a stand-in. 67 | -------------------------------------------------------------------------------- /lib/rb-inotify/native/flags.rb: -------------------------------------------------------------------------------- 1 | module INotify 2 | module Native 3 | # A module containing all the inotify flags 4 | # to be passed to {Notifier#watch}. 5 | # 6 | # @private 7 | module Flags 8 | # File was accessed. 9 | IN_ACCESS = 0x00000001 10 | # Metadata changed. 11 | IN_ATTRIB = 0x00000004 12 | # Writtable file was closed. 13 | IN_CLOSE_WRITE = 0x00000008 14 | # File was modified. 15 | IN_MODIFY = 0x00000002 16 | # Unwrittable file closed. 17 | IN_CLOSE_NOWRITE = 0x00000010 18 | # File was opened. 19 | IN_OPEN = 0x00000020 20 | # File was moved from X. 21 | IN_MOVED_FROM = 0x00000040 22 | # File was moved to Y. 23 | IN_MOVED_TO = 0x00000080 24 | # Subfile was created. 25 | IN_CREATE = 0x00000100 26 | # Subfile was deleted. 27 | IN_DELETE = 0x00000200 28 | # Self was deleted. 29 | IN_DELETE_SELF = 0x00000400 30 | # Self was moved. 31 | IN_MOVE_SELF = 0x00000800 32 | 33 | ## Helper events. 34 | 35 | # Close. 36 | IN_CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) 37 | # Moves. 38 | IN_MOVE = (IN_MOVED_FROM | IN_MOVED_TO) 39 | # All events which a program can wait on. 40 | IN_ALL_EVENTS = (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | 41 | IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | IN_MOVED_TO | IN_CREATE | 42 | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF) 43 | 44 | 45 | ## Special flags. 46 | 47 | # Only watch the path if it is a directory. 48 | IN_ONLYDIR = 0x01000000 49 | # Do not follow a sym link. 50 | IN_DONT_FOLLOW = 0x02000000 51 | # Add to the mask of an already existing watch. 52 | IN_MASK_ADD = 0x20000000 53 | # Only send event once. 54 | IN_ONESHOT = 0x80000000 55 | 56 | 57 | ## Events sent by the kernel. 58 | 59 | # Backing fs was unmounted. 60 | IN_UNMOUNT = 0x00002000 61 | # Event queued overflowed. 62 | IN_Q_OVERFLOW = 0x00004000 63 | # File was ignored. 64 | IN_IGNORED = 0x00008000 65 | # Event occurred against dir. 66 | IN_ISDIR = 0x40000000 67 | 68 | # Converts a list of flags to the bitmask that the C API expects. 69 | # 70 | # @param flags [Array] 71 | # @return [Fixnum] 72 | def self.to_mask(flags) 73 | flags.map {|flag| const_get("IN_#{flag.to_s.upcase}")}. 74 | inject(0) {|mask, flag| mask | flag} 75 | end 76 | 77 | # Converts a bitmask from the C API into a list of flags. 78 | # 79 | # @param mask [Fixnum] 80 | # @return [Array] 81 | def self.from_mask(mask) 82 | constants.map {|c| c.to_s}.select do |c| 83 | next false unless c =~ /^IN_/ 84 | const_get(c) & mask != 0 85 | end.map {|c| c.sub("IN_", "").downcase.to_sym} - [:all_events] 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/rb-inotify/watcher.rb: -------------------------------------------------------------------------------- 1 | module INotify 2 | # Watchers monitor a single path for changes, 3 | # specified by {INotify::Notifier#watch event flags}. 4 | # A watcher is usually created via \{Notifier#watch}. 5 | # 6 | # One {Notifier} may have many {Watcher}s. 7 | # The Notifier actually takes care of the checking for events, 8 | # via \{Notifier#run #run} or \{Notifier#process #process}. 9 | # The main purpose of having Watcher objects 10 | # is to be able to disable them using \{#close}. 11 | class Watcher 12 | # The {Notifier} that this Watcher belongs to. 13 | # 14 | # @return [Notifier] 15 | attr_reader :notifier 16 | 17 | # The path that this Watcher is watching. 18 | # 19 | # @return [String] 20 | attr_reader :path 21 | 22 | # The {INotify::Notifier#watch flags} 23 | # specifying the events that this Watcher is watching for, 24 | # and potentially some options as well. 25 | # 26 | # @return [Array] 27 | attr_reader :flags 28 | 29 | # The id for this Watcher. 30 | # Used to retrieve this Watcher from {Notifier#watchers}. 31 | # 32 | # @private 33 | # @return [Fixnum] 34 | attr_reader :id 35 | 36 | # Calls this Watcher's callback with the given {Event}. 37 | # 38 | # @private 39 | # @param event [Event] 40 | def callback!(event) 41 | @callback[event] 42 | end 43 | 44 | # Disables this Watcher, so that it doesn't fire any more events. 45 | # 46 | # @raise [SystemCallError] if the watch fails to be disabled for some reason 47 | def close 48 | return if Native.inotify_rm_watch(@notifier.fd, @id) == 0 49 | raise SystemCallError.new("Failed to stop watching #{path.inspect}", FFI.errno) 50 | end 51 | 52 | # Creates a new {Watcher}. 53 | # 54 | # @private 55 | # @see Notifier#watch 56 | def initialize(notifier, path, *flags, &callback) 57 | @notifier = notifier 58 | @callback = callback || proc {} 59 | @path = path 60 | @flags = flags.freeze 61 | @id = Native.inotify_add_watch(@notifier.fd, path.dup, 62 | Native::Flags.to_mask(flags)) 63 | 64 | unless @id < 0 65 | @notifier.watchers[@id] = self 66 | return 67 | end 68 | 69 | raise SystemCallError.new( 70 | "Failed to watch #{path.inspect}" + 71 | case FFI.errno 72 | when Errno::EACCES::Errno; ": read access to the given file is not permitted." 73 | when Errno::EBADF::Errno; ": the given file descriptor is not valid." 74 | when Errno::EFAULT::Errno; ": path points outside of the process's accessible address space." 75 | when Errno::EINVAL::Errno; ": the given event mask contains no legal events; or fd is not an inotify file descriptor." 76 | when Errno::ENOMEM::Errno; ": insufficient kernel memory was available." 77 | when Errno::ENOSPC::Errno; ": The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource." 78 | else; "" 79 | end, 80 | FFI.errno) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/rb-inotify/event.rb: -------------------------------------------------------------------------------- 1 | module INotify 2 | # An event caused by a change on the filesystem. 3 | # Each {Watcher} can fire many events, 4 | # which are passed to that watcher's callback. 5 | class Event 6 | # A list of other events that are related to this one. 7 | # Currently, this is only used for files that are moved within the same directory: 8 | # the `:moved_from` and the `:moved_to` events will be related. 9 | # 10 | # @return [Array] 11 | attr_reader :related 12 | 13 | # The name of the file that the event occurred on. 14 | # This is only set for events that occur on files in directories; 15 | # otherwise, it's `""`. 16 | # Similarly, if the event is being fired for the directory itself 17 | # the name will be `""` 18 | # 19 | # This pathname is relative to the enclosing directory. 20 | # For the absolute pathname, use \{#absolute\_name}. 21 | # Note that when the `:recursive` flag is passed to {Notifier#watch}, 22 | # events in nested subdirectories will still have a `#name` field 23 | # relative to their immediately enclosing directory. 24 | # For example, an event on the file `"foo/bar/baz"` 25 | # will have name `"baz"`. 26 | # 27 | # @return [String] 28 | attr_reader :name 29 | 30 | # The {Notifier} that fired this event. 31 | # 32 | # @return [Notifier] 33 | attr_reader :notifier 34 | 35 | # An integer specifying that this event is related to some other event, 36 | # which will have the same cookie. 37 | # 38 | # Currently, this is only used for files that are moved within the same directory. 39 | # Both the `:moved_from` and the `:moved_to` events will have the same cookie. 40 | # 41 | # @private 42 | # @return [Fixnum] 43 | attr_reader :cookie 44 | 45 | # The {Watcher#id id} of the {Watcher} that fired this event. 46 | # 47 | # @private 48 | # @return [Fixnum] 49 | attr_reader :watcher_id 50 | 51 | # Returns the {Watcher} that fired this event. 52 | # 53 | # @return [Watcher] 54 | def watcher 55 | @watcher ||= @notifier.watchers[@watcher_id] 56 | end 57 | 58 | # The absolute path of the file that the event occurred on. 59 | # 60 | # This is actually only as absolute as the path passed to the {Watcher} 61 | # that created this event. 62 | # However, it is relative to the working directory, 63 | # assuming that hasn't changed since the watcher started. 64 | # 65 | # @return [String] 66 | def absolute_name 67 | return watcher.path if name.empty? 68 | return File.join(watcher.path, name) 69 | end 70 | 71 | # Returns the flags that describe this event. 72 | # This is generally similar to the input to {Notifier#watch}, 73 | # except that it won't contain options flags nor `:all_events`, 74 | # and it may contain one or more of the following flags: 75 | # 76 | # `:unmount` 77 | # : The filesystem containing the watched file or directory was unmounted. 78 | # 79 | # `:ignored` 80 | # : The \{#watcher watcher} was closed, or the watched file or directory was deleted. 81 | # 82 | # `:isdir` 83 | # : The subject of this event is a directory. 84 | # 85 | # @return [Array] 86 | def flags 87 | @flags ||= Native::Flags.from_mask(@native[:mask]) 88 | end 89 | 90 | # Constructs an {Event} object from a string of binary data, 91 | # and destructively modifies the string to get rid of the initial segment 92 | # used to construct the Event. 93 | # 94 | # @private 95 | # @param data [String] The string to be modified 96 | # @param notifier [Notifier] The {Notifier} that fired the event 97 | # @return [Event, nil] The event, or `nil` if the string is empty 98 | def self.consume(data, notifier) 99 | return nil if data.empty? 100 | ev = new(data, notifier) 101 | data.replace data[ev.size..-1] 102 | ev 103 | end 104 | 105 | # Creates an event from a string of binary data. 106 | # Differs from {Event.consume} in that it doesn't modify the string. 107 | # 108 | # @private 109 | # @param data [String] The data string 110 | # @param notifier [Notifier] The {Notifier} that fired the event 111 | def initialize(data, notifier) 112 | ptr = FFI::MemoryPointer.from_string(data) 113 | @native = Native::Event.new(ptr) 114 | @related = [] 115 | @cookie = @native[:cookie] 116 | @name = data[@native.size, @native[:len]].gsub(/\0+$/, '') 117 | @notifier = notifier 118 | @watcher_id = @native[:wd] 119 | 120 | raise Exception.new("inotify event queue has overflowed.") if @native[:mask] & Native::Flags::IN_Q_OVERFLOW != 0 121 | end 122 | 123 | # Calls the callback of the watcher that fired this event, 124 | # passing in the event itself. 125 | # 126 | # @private 127 | def callback! 128 | watcher.callback!(self) 129 | end 130 | 131 | # Returns the size of this event object in bytes, 132 | # including the \{#name} string. 133 | # 134 | # @return [Fixnum] 135 | def size 136 | @native.size + @native[:len] 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/rb-inotify/notifier.rb: -------------------------------------------------------------------------------- 1 | module INotify 2 | # Notifier wraps a single instance of inotify. 3 | # It's possible to have more than one instance, 4 | # but usually unnecessary. 5 | # 6 | # @example 7 | # # Create the notifier 8 | # notifier = INotify::Notifier.new 9 | # 10 | # # Run this callback whenever the file path/to/foo.txt is read 11 | # notifier.watch("path/to/foo.txt", :access) do 12 | # puts "Foo.txt was accessed!" 13 | # end 14 | # 15 | # # Watch for any file in the directory being deleted 16 | # # or moved out of the directory. 17 | # notifier.watch("path/to/directory", :delete, :moved_from) do |event| 18 | # # The #name field of the event object contains the name of the affected file 19 | # puts "#{event.name} is no longer in the directory!" 20 | # end 21 | # 22 | # # Nothing happens until you run the notifier! 23 | # notifier.run 24 | class Notifier 25 | # A list of directories that should never be recursively watched. 26 | # 27 | # * Files in `/dev/fd` sometimes register as directories, but are not enumerable. 28 | RECURSIVE_BLACKLIST = %w[/dev/fd] 29 | 30 | # A hash from {Watcher} ids to the instances themselves. 31 | # 32 | # @private 33 | # @return [{Fixnum => Watcher}] 34 | attr_reader :watchers 35 | 36 | # The underlying file descriptor for this notifier. 37 | # This is a valid OS file descriptor, and can be used as such 38 | # (except under JRuby -- see \{#to\_io}). 39 | # 40 | # @return [Fixnum] 41 | attr_reader :fd 42 | 43 | # @return [Boolean] Whether or not this Ruby implementation supports 44 | # wrapping the native file descriptor in a Ruby IO wrapper. 45 | def self.supports_ruby_io? 46 | RUBY_PLATFORM !~ /java/ 47 | end 48 | 49 | # Creates a new {Notifier}. 50 | # 51 | # @return [Notifier] 52 | # @raise [SystemCallError] if inotify failed to initialize for some reason 53 | def initialize 54 | @fd = Native.inotify_init 55 | @watchers = {} 56 | return unless @fd < 0 57 | 58 | raise SystemCallError.new( 59 | "Failed to initialize inotify" + 60 | case FFI.errno 61 | when Errno::EMFILE::Errno; ": the user limit on the total number of inotify instances has been reached." 62 | when Errno::ENFILE::Errno; ": the system limit on the total number of file descriptors has been reached." 63 | when Errno::ENOMEM::Errno; ": insufficient kernel memory is available." 64 | else; "" 65 | end, 66 | FFI.errno) 67 | end 68 | 69 | # Returns a Ruby IO object wrapping the underlying file descriptor. 70 | # Since this file descriptor is fully functional (except under JRuby), 71 | # this IO object can be used in any way a Ruby-created IO object can. 72 | # This includes passing it to functions like `#select`. 73 | # 74 | # Note that this always returns the same IO object. 75 | # Creating lots of IO objects for the same file descriptor 76 | # can cause some odd problems. 77 | # 78 | # **This is not supported under JRuby**. 79 | # JRuby currently doesn't use native file descriptors for the IO object, 80 | # so we can't use this file descriptor as a stand-in. 81 | # 82 | # @return [IO] An IO object wrapping the file descriptor 83 | # @raise [NotImplementedError] if this is being called in JRuby 84 | def to_io 85 | unless self.class.supports_ruby_io? 86 | raise NotImplementedError.new("INotify::Notifier#to_io is not supported under JRuby") 87 | end 88 | @io ||= IO.new(@fd) 89 | end 90 | 91 | # Watches a file or directory for changes, 92 | # calling the callback when there are. 93 | # This is only activated once \{#process} or \{#run} is called. 94 | # 95 | # **Note that by default, this does not recursively watch subdirectories 96 | # of the watched directory**. 97 | # To do so, use the `:recursive` flag. 98 | # 99 | # ## Flags 100 | # 101 | # `:access` 102 | # : A file is accessed (that is, read). 103 | # 104 | # `:attrib` 105 | # : A file's metadata is changed (e.g. permissions, timestamps, etc). 106 | # 107 | # `:close_write` 108 | # : A file that was opened for writing is closed. 109 | # 110 | # `:close_nowrite` 111 | # : A file that was not opened for writing is closed. 112 | # 113 | # `:modify` 114 | # : A file is modified. 115 | # 116 | # `:open` 117 | # : A file is opened. 118 | # 119 | # ### Directory-Specific Flags 120 | # 121 | # These flags only apply when a directory is being watched. 122 | # 123 | # `:moved_from` 124 | # : A file is moved out of the watched directory. 125 | # 126 | # `:moved_to` 127 | # : A file is moved into the watched directory. 128 | # 129 | # `:create` 130 | # : A file is created in the watched directory. 131 | # 132 | # `:delete` 133 | # : A file is deleted in the watched directory. 134 | # 135 | # `:delete_self` 136 | # : The watched file or directory itself is deleted. 137 | # 138 | # `:move_self` 139 | # : The watched file or directory itself is moved. 140 | # 141 | # ### Helper Flags 142 | # 143 | # These flags are just combinations of the flags above. 144 | # 145 | # `:close` 146 | # : Either `:close_write` or `:close_nowrite` is activated. 147 | # 148 | # `:move` 149 | # : Either `:moved_from` or `:moved_to` is activated. 150 | # 151 | # `:all_events` 152 | # : Any event above is activated. 153 | # 154 | # ### Options Flags 155 | # 156 | # These flags don't actually specify events. 157 | # Instead, they specify options for the watcher. 158 | # 159 | # `:onlydir` 160 | # : Only watch the path if it's a directory. 161 | # 162 | # `:dont_follow` 163 | # : Don't follow symlinks. 164 | # 165 | # `:mask_add` 166 | # : Add these flags to the pre-existing flags for this path. 167 | # 168 | # `:oneshot` 169 | # : Only send the event once, then shut down the watcher. 170 | # 171 | # `:recursive` 172 | # : Recursively watch any subdirectories that are created. 173 | # Note that this is a feature of rb-inotify, 174 | # rather than of inotify itself, which can only watch one level of a directory. 175 | # This means that the {Event#name} field 176 | # will contain only the basename of the modified file. 177 | # When using `:recursive`, {Event#absolute_name} should always be used. 178 | # 179 | # @param path [String] The path to the file or directory 180 | # @param flags [Array] Which events to watch for 181 | # @yield [event] A block that will be called 182 | # whenever one of the specified events occur 183 | # @yieldparam event [Event] The Event object containing information 184 | # about the event that occured 185 | # @return [Watcher] A Watcher set up to watch this path for these events 186 | # @raise [SystemCallError] if the file or directory can't be watched, 187 | # e.g. if the file isn't found, read access is denied, 188 | # or the flags don't contain any events 189 | def watch(path, *flags, &callback) 190 | return Watcher.new(self, path, *flags, &callback) unless flags.include?(:recursive) 191 | 192 | dir = Dir.new(path) 193 | 194 | dir.each do |base| 195 | d = File.join(path, base) 196 | binary_d = d.respond_to?(:force_encoding) ? d.dup.force_encoding('BINARY') : d 197 | next if binary_d =~ /\/\.\.?$/ # Current or parent directory 198 | watch(d, *flags, &callback) if !RECURSIVE_BLACKLIST.include?(d) && File.directory?(d) 199 | end 200 | 201 | dir.close 202 | 203 | rec_flags = [:create, :moved_to] 204 | return watch(path, *((flags - [:recursive]) | rec_flags)) do |event| 205 | callback.call(event) if flags.include?(:all_events) || !(flags & event.flags).empty? 206 | next if (rec_flags & event.flags).empty? || !event.flags.include?(:isdir) 207 | begin 208 | watch(event.absolute_name, *flags, &callback) 209 | rescue Errno::ENOENT 210 | # If the file has been deleted since the glob was run, we don't want to error out. 211 | end 212 | end 213 | end 214 | 215 | # Starts the notifier watching for filesystem events. 216 | # Blocks until \{#stop} is called. 217 | # 218 | # @see #process 219 | def run 220 | @stop = false 221 | process until @stop 222 | end 223 | 224 | # Stop watching for filesystem events. 225 | # That is, if we're in a \{#run} loop, 226 | # exit out as soon as we finish handling the events. 227 | def stop 228 | @stop = true 229 | end 230 | 231 | # Blocks until there are one or more filesystem events 232 | # that this notifier has watchers registered for. 233 | # Once there are events, the appropriate callbacks are called 234 | # and this function returns. 235 | # 236 | # @see #run 237 | def process 238 | read_events.each {|event| event.callback!} 239 | end 240 | 241 | # Close the notifier. 242 | # 243 | # @raise [SystemCallError] if closing the underlying file descriptor fails. 244 | def close 245 | return if Native.close(@fd) == 0 246 | 247 | raise SystemCallError.new("Failed to properly close inotify socket" + 248 | case FFI.errno 249 | when Errno::EBADF::Errno; ": invalid or closed file descriptior" 250 | when Errno::EIO::Errno; ": an I/O error occured" 251 | end, 252 | FFI.errno) 253 | end 254 | 255 | # Blocks until there are one or more filesystem events 256 | # that this notifier has watchers registered for. 257 | # Once there are events, returns their {Event} objects. 258 | # 259 | # {#run} or {#process} are ususally preferable to calling this directly. 260 | def read_events 261 | size = 64 * Native::Event.size 262 | tries = 1 263 | 264 | begin 265 | data = readpartial(size) 266 | rescue SystemCallError => er 267 | # EINVAL means that there's more data to be read 268 | # than will fit in the buffer size 269 | raise er unless er.errno == Errno::EINVAL::Errno || tries == 5 270 | size *= 2 271 | tries += 1 272 | retry 273 | end 274 | 275 | events = [] 276 | cookies = {} 277 | while event = Event.consume(data, self) 278 | events << event 279 | next if event.cookie == 0 280 | cookies[event.cookie] ||= [] 281 | cookies[event.cookie] << event 282 | end 283 | cookies.each {|c, evs| evs.each {|ev| ev.related.replace(evs - [ev]).freeze}} 284 | events 285 | end 286 | 287 | private 288 | 289 | # Same as IO#readpartial, or as close as we need. 290 | def readpartial(size) 291 | # Use Ruby's readpartial if possible, to avoid blocking other threads. 292 | return to_io.readpartial(size) if self.class.supports_ruby_io? 293 | 294 | tries = 0 295 | begin 296 | tries += 1 297 | buffer = FFI::MemoryPointer.new(:char, size) 298 | size_read = Native.read(fd, buffer, size) 299 | return buffer.read_string(size_read) if size_read >= 0 300 | end while FFI.errno == Errno::EINTR::Errno && tries <= 5 301 | 302 | raise SystemCallError.new("Error reading inotify events" + 303 | case FFI.errno 304 | when Errno::EAGAIN::Errno; ": no data available for non-blocking I/O" 305 | when Errno::EBADF::Errno; ": invalid or closed file descriptor" 306 | when Errno::EFAULT::Errno; ": invalid buffer" 307 | when Errno::EINVAL::Errno; ": invalid file descriptor" 308 | when Errno::EIO::Errno; ": I/O error" 309 | when Errno::EISDIR::Errno; ": file descriptor is a directory" 310 | else; "" 311 | end, 312 | FFI.errno) 313 | end 314 | end 315 | end 316 | --------------------------------------------------------------------------------