2018-03-20

rb-inotifyコードリーディング

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

参考URL

このエントリーをはてなブックマークに追加