rb-inotify (0.9.10)のコードリーディングをしました。listenで利用されているgemです。
inotifyはLinuxの機能になるのでLinuxでのみ動作します。macOSの場合はFSEventsを使ったrb-fseventでファイル監視が可能です。使い方は以下のように記述します。
notifier = INotify::Notifier.new
notifier.watch("path/to/foo.txt", :modify) { puts "foo.txt was modified!" }
notifier.watch("path/to/bar", :moved_to, :create) do |event|
puts "#{event.name} is now in path/to/bar!"
end
notifier.run
INotify::Notifierのコンストラクタは以下のように定義されており、INotify::Native.inotify_initを呼び出します。
module INotify
class Notifier
def initialize
@fd = Native.inotify_init
@watchers = {}
return unless @fd < 0
# ...
end
INotify::Notifier#watchはINotify::Watcherのインスタンスを生成します。
def watch(path, *flags, &callback)
return Watcher.new(self, path, *flags, &callback) unless flags.include?(:recursive)
# ...
end
INotify::Watcherのコンストラクタ内では、Native.inotify_add_watchを呼び出して監視パスを追加し、自身をNotifierのwatchers変数に追加します。
module INotify
class Watcher
def initialize(notifier, path, *flags, &callback)
@notifier = notifier
@callback = callback || proc {}
@path = path
@flags = flags.freeze
@id = Native.inotify_add_watch(@notifier.fd, path.dup,
Native::Flags.to_mask(flags))
unless @id < 0
@notifier.watchers[@id] = self
return
end
# ...
end
end
end
INotify::Notifier#runは#processを呼び出し、#read_eventsで取得したイベントに対してコールバックを呼び出します。
def run
@stop = false
process until @stop
end
def stop
@stop = true
end
def process
read_events.each do |event|
event.callback!
event.flags.include?(:ignored) && event.notifier.watchers.delete(event.watcher_id)
end
end
#read_eventsは#readpartialを呼び出してデータを取得しEventの配列を返します。
def read_events
size = Native::Event.size + Native.fpathconf(fd, Native::Flags::PC_NAME_MAX) + 1
tries = 1
begin
data = readpartial(size)
rescue SystemCallError => er
# ...
end
return [] if data.nil?
events = []
cookies = {}
while event = Event.consume(data, self)
events << event
next if event.cookie == 0
cookies[event.cookie] ||= []
cookies[event.cookie] << event
end
cookies.each {|c, evs| evs.each {|ev| ev.related.replace(evs - [ev]).freeze}}
events
end
#readpartialはイベントデータを取得しています。to_ioはNotifer.inotify_initで取得したファイルディスクリプタのIOオブジェクトです。IO#readpartialを呼び出すことでinotifyのイベントが来るまでブロックします。
def readpartial(size)
# Use Ruby's readpartial if possible, to avoid blocking other threads.
begin
return to_io.readpartial(size) if self.class.supports_ruby_io?
# ...
end
Event#callback!は以下のような定義になっています。@watcher_idはinotify_add_watchで返された監視用のディスクリプタで、これをキーにNotifier#watchersからWatcherのインスタンスを取得し、Watcher#callback!を実行します。Watcher#callback!で実行される処理はNotifier#watchで指定したブロックになります。
module INotify
class Event
attr_reader :watcher_id
def watcher
@watcher ||= @notifier.watchers[@watcher_id]
end
def callback!
watcher && watcher.callback!(self)
end
Native.inotify_initやNative.inotify_add_watchはffiを使ってC関数を呼び出せるようにしています。
require 'ffi'
module INotify
module Native
extend FFI::Library
ffi_lib FFI::Library::LIBC
begin
ffi_lib 'inotify'
rescue LoadError
end
class Event < FFI::Struct
layout(
:wd, :int,
:mask, :uint32,
:cookie, :uint32,
:len, :uint32)
end
attach_function :inotify_init, [], :int
attach_function :inotify_add_watch, [:int, :string, :uint32], :int
attach_function :inotify_rm_watch, [:int, :uint32], :int
attach_function :fpathconf, [:int, :int], :long
attach_function :read, [:int, :pointer, :size_t], :ssize_t
attach_function :close, [:int], :int
end
end