AnyEventで指定したパス以下の変化を検知する

 指定したパス以下の変更を検知するのってどうすればいいかな、ということにちょっと興味が湧いたので、調べてみました。
 ここでは、Windowsが手元にないのでパスするとして、LinuxMacOSX(BSD)について触れようと思います。

そもそもOSの検知システムは何があって、Perlからは何でアクセスできるか

 以下に対応表を作成しましたので、参照ください。

OS 検知システム Perl binding
Linux inotify Linux::Inotify2
BSD kqueue IO::KQueue
Mac OS X kqueue IO::KQueue
Mac OS X FSEvents Mac::FSEvents

 OSXBSDベースのUNIX系OSのため、自前のFSEventsだけでなく、KQueueも使用できます。

実装に際しての戦略(生存。。。じゃないよ)

 おおよそ、以下の2点に絞られると思います。

  1. 定期的に検知システムでチェックを行い、変更があればイベントとしてオブジェクトを発行する
  2. 検知システムの変化を検知し、イベントを発行する(つまりAnyEventの出番)

 仮にもここでは、1番目をロングボール戦略、2番目をイベント戦略とします。
 ロングボール戦略については、各ライブラリのsynopsisにあるサンプルコードが大体がそうなってるので、特に説明は要らないですね。

とゆーことで、イベント戦略について

 AnyEventでイベントを監視するには、ioメソッドを使うのが一般的です、というかそれしかない(AE::Handleは内部的にはAE::ioなので)。つまり、何をAE::ioで監視するかが焦点になります。
 さてここで、kqueue、inotify、FSEventsそれぞれのmanを確認してみます。

  • kqueue

kqueue システムコールは新規のカーネルイベントキューを生成して記述子を返します。

http://www.lemoda.net/tools/man/ja/2/kqueue
  • inotify

inotify_init(2) は inotify インスタンスを作成し、inotify インスタンスを参照する ファイルディスクリプタを返す。

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man7/inotify.7.html
  • FSEvents

299 fh = fdopen( self->respipe[0], "r" );
300
301 RETVAL = fh;

https://metacpan.org/source/RHOELZ/Mac-FSEvents-0.05/FSEvents.xs

 すいません、FSEventsだけはちょっと場合が違いました。XS側でファイルディスクリプタを開くようにしていますね。まあ、結果オーライで><
 いづれにしろ共通点は何かというと、検知システムはファイルディスクリプタを必ず持っているということです。
 ここで、AE::ioの引数を振り返ってみると、fhのキー(もしくは第1引数)にはファイルハンドルまたはfileno(ファイルディスクリプタ)を指定せよとあります。
 つまり、検知システムのファイルディスクリプタをAE::ioの引数に渡すことで、AnyEventで検知システムのイベントをハンドリングできるようになるということです!
 IO::KQueueを使ってその実装サンプルを作ってみました。

#!perl -w

#example: KQueueでの実装例。なぜなら一番特殊だったから。。。
use strict;
use warnings;

use AnyEvent;
use IO::KQueue;

my $_callback = sub {
    print "ダァ!!シエリイェス!!";
};

my $kqueue = IO::KQueue->new;
open my $fh, '<', '/path/to/watch' or die;
$kqueue->EV_SET(
    fileno($fh)
    EVFILT_VNODE,
    EV_ADD | EV_CLEAR,
    NOTE_DELETE | NOTE_WRITE | NOTE_RENAME | NOTE_REVOKE
);
my $w = AE::io $$kqueue, 0, sub {
    if (my @events = $kqueue->kevent) {
        $_callback->(@events);
    }
};

AE::cv->recv;

 IO::KQueueのファイルディスクリプタインスタンススカラーデリファレンスすることで取得できますが、Linux::Inotify2ではfilenoメソッドでファイルディスクリプタを、Mac::FSEventsではwatchメソッドでファイルハンドルをそれぞれ取得することが出来ます。

Filesys::Notify::Simpleについて

 今年9月に、miyagawaさんがFilesys::Notify::Simpleというモジュールをアップしています。inotify、kqueue、FSEventsだけでなくタイマーでの監視が含まれているので、恐らく(windowsも含め)どの環境でも動くと思います。
 自分はAE::timerを使ったアプローチで一度実装できたあとにこのモジュールを見たので、やっぱりもうあったか。。。と気落ちしていましたが、これはロングボール戦略(というかライブラリ備え付けの自前のタイマー)での実装なので、AnyEvent系のモジュールと併用するときに問題が出るかな、と。
 それじゃあ自分はもっとAnyEventベースのものを作ってみよう、と思って悪戦苦闘した結果。。。

ついにAnyEventベースのラッパーライブラリができましたっ(キリッ

 →GitHub - taiyoh/p5-AnyEvent-FileWatch
 AnyEvent::Filesys::Notify::... なんて長すぎるので、ちょっと名前変えました。
 このエントリを書くために大急ぎで作ったモジュールです。アルファリリースなんてもんじゃないです。かなりグダグダなので。
 動作サンプルはこんな感じです。インターフェイスはFilesys::Notify::Simpleに合わせています。現状は、ひとまず変化があるとなんかコールバックが呼ばれる、くらいのものです。ひどいです。

#!perl -w

use strict;
use warnings;

use AnyEvent::FileWatch;
use Data::Dumper;

my $filepath = shift or die;

# 内部的にInotify/FSEvents/KQueueを切り替えてます
my $fw = AnyEvent::FileWatch->new([$filepath]);

$fw->wait(sub {
    my @events = @_;
    print Dumper(\@events);
});

AE::cv->recv;

 因みに言うと、監視系モジュールの多くがMooseを使っていて、それほどのもんじゃないっちゅーねん、とちょっとげんなりしています。

まとめ

  • はじめてまともにXS読んだ(書いてないけど)
  • はじめてkqueueを使ってみた(Cを書いてちょっと動作を見てました)
  • AnyEventについてちょっと理解が深まった
  • 実は未だに合ってるかどうか不安
  • AnyEvent使わないのであれば、Filesys::Notify::Simpleを使う方がいいです

 ご指摘についてはお手柔らかにいただけますと幸いです><