Haskell でファイルシステムの変更を検出する
この記事は Haskell Advent Calendar 2013 の 15 日目の記事です。
Haskell でファイルシステムの変更を検出する方法を調査してみました。
いきなり Java で恐縮ですが、イメージとしては以下の様な感じです。
import java.nio.file.*; import static java.nio.file.StandardWatchEventKinds.*; public class Watcher { public static void main(String[] args) throws Exception { Path dir = Paths.get("."); WatchService watcher = FileSystems.getDefault().newWatchService(); dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); for (WatchKey key = watcher.take(); ; key.reset()) { for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); if (kind == OVERFLOW) { continue; } System.out.format("%s: %s\n", kind, event.context()); } } } }
今回は Haskell 主体の記事なので、上記コードの説明は割愛します。以下のようにコンパイル、実行します。
C:\works>javac Watcher.java C:\works>java Watcher ENTRY_DELETE: text.txt ENTRY_CREATE: text.txt ENTRY_MODIFY: text.txt
出力されているログは、text.txt というファイルを秀丸エディタで上書き保存した際のものです。上書き保存は内部的には削除、作成、変更という手順で行われているんですね。興味深いです。
さて、Haskell で Java の WatchService 相当が行えるモジュールとして、hackage に fsnotify が公開されています。
これを使用できるように cabal でインストールするには、以下のように行います。
C:\works>cabal install fsnotify
このモジュールを使って実装したファイルシステム監視関数は以下のとおりです。
module Watcher (watch) where import Control.Concurrent import Filesystem (getWorkingDirectory) import System.FSNotify import System.Exit import System.IO.Error watch :: (Event -> IO ()) -> IO () watch f = do dir <- getWorkingDirectory withManager $ \manager -> do watchDir manager dir (const True) $ \event -> f event waitBreak where waitBreak = do _ <- catchIOError getLine (\e -> if isEOFError e then exitSuccess else exitFailure) waitBreak
ここで使用している watchDir という関数が、fsnotify の提供するファイルシステム監視関数です。以下の様な定義になっています。
data Event = Added FilePath UTCTime | Modified FilePath UTCTime | Removed FilePath UTCTime deriving (Eq, Show) type ActionPredicate = Event -> Bool type Action = Event -> IO () watchDir :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO ()
Event は追加、変更、削除があり、ファイルパスとタイムスタンプを持っています。
ActionPredicate は、Event を受け取り Bool を返すフィルタ関数の定義です。今回の実装では (const True) で常に True を返すようにしています。
Action は Event を受け取り IO () を返すユーザ定義コールバック関数です。今回の実装ではこのコールバック関数を外部から渡せるようにしています。
waitBreak という関数は、Ctrl+C で割り込みをした際にプロセスを終了させるために存在します。これは必ずしも必要ではなく、Java の挙動に合わせた感じです。
この関数を呼び出すサンプルコードは以下のとおりです。
import Watcher main = watch print
コールバック関数として print を渡し、Event がそのまま表示されるようにしています。
これを以下のようにコンパイル、実行します。
C:\works>ghc WatcherTest.hs C:\works>WatcherTest.exe Removed (FilePath "text.txt") 2013-12-15 12:04:55.6163553 UTC Added (FilePath "text.txt") 2013-12-15 12:04:55.6223556 UTC Modified (FilePath "text.txt") 2013-12-15 12:04:55.6273559 UTC
同様に秀丸で text.txt を上書き更新した際のログです。大体同じように動作することが確認できました。
使い方も結構簡単だし、fsnotify はマルチプラットフォームだし、watchDir の代わりに watchTree を使用すればサブディレクトリを再帰的に検知するし、めでたし、めでたし。
・・・、では終わらないんですね。
fsnotify を使用して気がついた点をいくつかピックアップします。
ファイルシステム変更全般
Java の場合、
監視対象のディレクトリ内のファイルが変更されたことを知らせるイベントが報告された場合、そのファイルを変更したプログラムが完了しているという保証はありません。
WatchService (Java Platform SE 7)
という記載が Javadoc にあります。これは、Java が動作するプラットフォームやファイルシステム、ポーティングレイヤーの実装に依存し、検出が同期だったり非同期だったりするためです。通知はあれど未完、というのは扱いにくいですね。
fsnotify にはこうした記載はないのですが、おそらく同様の考え方が必要になるはずです。
イベントをかなりの確率で取りこぼす
原因はよくわかりません。取りこぼし率は、体感で 3 割以上はありそうです。特に Added が通知されないことが多いように感じます。
fsnotify の実装を調べてみたところ、内部では同一作者の実装する System.Win32.Notify というモジュールが使用されており、そこでは Win32 API の ReadDirectoryChangesW が使用されていました。丁寧にデバッグすれば原因が System.Win32.Notify モジュールにあるのか、fsnotify にあるのか、それとも今回のサンプルコードにあるのかはわかるかも知れませんが*1、今回はそこまでは追いかけていません。
本気で使うときに本気でデバッグしてみようと思います。
それでは皆さん、ハッピークリスマス!