読者です 読者をやめる 読者になる 読者になる

WatchService で再帰的にディレクトリ監視で難あり


Java7 で追加された WatchService は指定したディレクトリ内でファイルまたはディレクトリの 作成、変更、削除を追跡できる便利なクラスです。
けど、指定したディレクトリ配下を再帰的に監視するには、自力でガンバレです。


ということで、再帰的に監視してみます。

public class RecursiveWatcher {
  
  private static Logger log = Logger.getLogger(RecursiveWatcher.class.getName());
  
  private final WatchService watcher = FileSystems.getDefault().newWatchService();
  private final Map<WatchKey, Path> watchKeys = new HashMap<>();
  private List<Callback> callbacks = new ArrayList<>();
  
  interface Callback { void onCall(WatchEvent<Path> event, Path path);}

  
  public RecursiveWatcher(Path dir, Callback... callbacks) throws IOException {
    this.callbacks.addAll(Arrays.asList(callbacks));
    registerRecursive(dir);
  }
  
  private void registerRecursive(final Path base) { try {
    Files.walkFileTree(base, new SimpleFileVisitor<Path>() {
      @Override
      public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        register(dir);
        return FileVisitResult.CONTINUE;
      }
    });
  } catch(IOException e) { throw new RuntimeException(e);} }

  
  private void register(final Path dir) { try {
    if (dir == null) return;
    WatchKey key = dir.register(this.watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
    this.watchKeys.put(key, dir);
  } catch(IOException e) { throw new RuntimeException(e);} }
  

  public void start() { try {
    while (true) {
      WatchKey key;
      try {
        key = this.watcher.take(); // wait
      } catch (InterruptedException e) {
        log.log(Level.WARNING, "interrupted watch service.", e);
        return;
      }

      for (WatchEvent<?> event: key.pollEvents()) {
        if (event.kind() == OVERFLOW) continue;

        WatchEvent<Path> watchEvent = cast(event);
        Path path = this.watchKeys.get(key).resolve(watchEvent.context());
        
        if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
          if (event.kind() == ENTRY_CREATE) {
            register(path);
          }
        } else if (Files.isRegularFile(path, NOFOLLOW_LINKS)) {
          if (event.kind() == ENTRY_CREATE || event.kind() == ENTRY_MODIFY) {
            for (Callback cb : this.callbacks) {cb.onCall(watchEvent, path);}
          }
        }
      }

      if (!key.reset()) {
        this.watchKeys.remove(key);
      }
      for (Iterator<Entry<WatchKey, Path>> it = this.watchKeys.entrySet().iterator(); it.hasNext();) {
        // sweep 
        Entry<WatchKey, Path> entry = it.next();
        if(Files.notExists(entry.getValue(), NOFOLLOW_LINKS)) {
          entry.getKey().cancel();
          it.remove();
        }
      }
      if (this.watchKeys.isEmpty()) break;
    }} finally { try {
      watcher.close();
    } catch (IOException e) {} }
  }
  
  @SuppressWarnings("unchecked")
  private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
    return (WatchEvent<T>)event;
  }

  
  public static void main(String[] args) throws IOException {
    new RecursiveWatcher(Paths.get("src"), new Callback(){
      public void onCall(WatchEvent<Path> event, Path path) {
        System.out.format("onCall: %s : %s\n", event.kind().name(), path);
      }
    }).start();
  }
}

やってることは

簡単で、最初にベースとなるディレクトリを指定して、Files の walkFileTree で再帰的にディレクトリを監視対象として追加してます。そいで、監視ディレクトリの中にディレクトリが新しく作成されたら、register() で登録しています。
ディレクトリが消されていたら、基本的には key.reset() が false で帰ってくるので監視対象から外してやります。

Windows環境だと、

ちょっと動きが不安定で、エクスプローラーで、中にディレクトリのあるディレクトリを削除すると、監視対象として親のディレクトリが残ってしまうことがあったので、sweep コメントの箇所で掃除しています。コマンドプロンプトでの操作だと大丈夫そうでしたが。

それよりもよろしくないのは、

Windows環境での話ですが(他の環境では見ていませんが)、以下のようなディレクトリ構成の場合

 target
  └ foo
     └ hoge.txt
 > cd target
 > rename foo bar

の操作はOK。

でも、以下のように foo 配下にディレクトリ hoge があると、

 target
  └ foo
     └ hoge
 > cd target
 > rename foo bar
 アクセスが拒否されました。

のようにディレクトリ名の変更が出来ません。

なんとかなりませんか?