Go
Mac

[Go][Mac] ファイルの変更を検知する方法

More than 1 year has passed since last update.

はじめに

本エントリでは、Go 言語を使い、Mac OS X 上でファイルの変更を検知するプログラムを作成します。

開発環境

  • fsnotify v1
  • Go 1.3
  • Mac OS X 10.9.3

fsnotify パッケージとは?

ここでは、fsnotify という、サードパーティーのパッケージを使います。

fsnotify は、ファイルの変更を検知するためのパッケージです。今回は、Mac OS X 上でプログラムを実行しますが、Windows や Linux などの OS にも対応しています。将来的には、標準パッケージへの統合を目指しているとのことです。

fsnotify パッケージのインストール

コマンドライン上で、以下のように、go getコマンドを実行します。

$ go get github.com/go-fsnotify/fsnotify

注)環境変数 $GOPATH

go get コマンドを実行する際には、環境変数 $GOPATH を設定しておく必要があります。$GOPATH は、パッケージのインストール先を示します。

例(bashの場合)

export GOPATH=$HOME/go # 指定するディレクトリは自由に決めてよい

ソースコード

以下に、今回作成したソースコードを示します。ファイル名は、sample_fsnotify.go としました。

sample_fsnotify.go
package main

import (
    "log"
    "github.com/go-fsnotify/fsnotify"
    "os"
    "path/filepath"
)

func main() {
    if len(os.Args) != 2 {
        log.Println("Usage: go run " + filepath.Base(os.Args[0]) + ".go directory_name")
        return
    }
    dirname := os.Args[1]

    watcher, err := fsnotify.NewWatcher() // (1)
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close()

    done := make(chan bool) // (2)

    go func() { // (3)
        for {
            select {
            case event:= <-watcher.Events:
                log.Println("event: ", event)
                switch {
                case event.Op&fsnotify.Write == fsnotify.Write:
                    log.Println("Modified file: ", event.Name)
                case event.Op&fsnotify.Create == fsnotify.Create:
                    log.Println("Created file: ", event.Name)
                case event.Op&fsnotify.Remove == fsnotify.Remove:
                    log.Println("Removed file: ", event.Name)
                case event.Op&fsnotify.Rename == fsnotify.Rename:
                    log.Println("Renamed file: ", event.Name)
                case event.Op&fsnotify.Chmod == fsnotify.Chmod:
                    log.Println("File changed permission: ", event.Name)
                }
            case err:= <-watcher.Errors:
                log.Println("error: ", err)
                done <-true // (4)
            }
        }
    }()

    err = watcher.Add(dirname) // (5)
    if err != nil {
        log.Fatal(err)
    }

    <-done // (6)
}

プログラムの実行方法

コマンドライン上で、以下のコマンドを実行します。引数には、監視対象のディレクトリのパスを設定します。

$ go run sample_fsnotify.go /Users/XXX/

プログラムの実行結果

上記のプログラムを実行させた状態で、ファイル操作(Create、Write、Rename、Remove、Chmod)を行うと、以下が出力されます。

$ go run sample_fsnotify.go /Users/XXX/

// ファイルの作成
2014/08/18 19:32:09 event:  "/Users/XXX/tmp.txt": CREATE
2014/08/18 19:32:09 Created file:  /Users/XXX/tmp.txt

// パーミッションの変更
2014/08/18 19:32:15 event:  "/Users/XXX/tmp.txt": CHMOD
2014/08/18 19:32:15 File changed permission:  /Users/XXX/tmp.txt

// ファイルへの書き込み
2014/08/18 19:32:15 event:  "/Users/XXX/tmp.txt": WRITE
2014/08/18 19:32:15 Modified file:  /Users/XXX/tmp.txt

// ファイル名の変更
2014/08/18 19:32:32 event:  "/Users/XXX/tmp.txt": RENAME
2014/08/18 19:32:32 Renamed file:  /Users/XXX/tmp.txt

// ファイルの削除
2014/08/18 19:32:34 event:  "/Users/XXX/tmp2.txt": REMOVE
2014/08/18 19:32:34 Removed file:  /Users/XXX/tmp2.txt

プログラムの終了方法

Ctrl + C でプログラムを終了します。

ソースコードの説明

(1)

上記のコードの (1) で、Watcher 型の変数を作成しています。ファイル変更の検知に関する操作は、この watcher 変数をとおして行っていきます。

watcher, err := fsnotify.NewWatcher() // (1)

Mac OS X の場合、Watcher 型は、fsnotify パッケージの fsnotify_bsd.go の中で定義されています。

fsnotify_bsd.go
type Watcher struct {
    Events          chan Event
    Errors          chan error
    mu              sync.Mutex          // Mutex for the Watcher itself.
    kq              int                 // File descriptor (as returned by the kqueue() syscall).
    watches         map[string]int      // Map of watched file descriptors (key: path).
    wmut            sync.Mutex          // Protects access to watches.
    enFlags         map[string]uint32   // Map of watched files to evfilt note flags used in kqueue.
    enmut           sync.Mutex          // Protects access to enFlags.
    paths           map[int]string      // Map of watched paths (key: watch descriptor).
    finfo           map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor).
    pmut            sync.Mutex          // Protects access to paths and finfo.
    fileExists      map[string]bool     // Keep track of if we know this file exists (to stop duplicate create events).
    femut           sync.Mutex          // Protects access to fileExists.
    externalWatches map[string]bool     // Map of watches added by user of the library.
    ewmut           sync.Mutex          // Protects access to externalWatches.
    done            chan bool           // Channel for sending a "quit message" to the reader goroutine
    isClosed        bool                // Set to true when Close() is first called
}

Watcher 型のフィールド変数のうち、重要なのは、以下の3つです。

  • Events
  • Errors
  • paths

Events

Events は、Event 型のチャネルです。Event 型は、「1つのファイルシステム通知」を表します。

fsnotify.go
// Event represents a single file system notification.
type Event struct {
    Name string // Relative path to the file or directory.
    Op   Op     // File operation that triggered the event.
}

Event 型のフィールド変数 Op は、そのイベントのトリガーとなったファイル操作(Create、Write、Rename、Remove、Chmod)を表します。

fsnotify_bsd.go
// newEvent returns an platform-independent Event based on kqueue Fflags.
func newEvent(name string, mask uint32, create bool) Event {
    e := Event{Name: name}
    if create {
        e.Op |= Create
    }
    if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE {
        e.Op |= Remove
    }
    if mask&syscall.NOTE_WRITE == syscall.NOTE_WRITE {
        e.Op |= Write
    }
    if mask&syscall.NOTE_RENAME == syscall.NOTE_RENAME {
        e.Op |= Rename
    }
    if mask&syscall.NOTE_ATTRIB == syscall.NOTE_ATTRIB {
        e.Op |= Chmod
    }
    return e
}

Op には、ビット毎に、ファイル操作の意味が割り当てられており、

  • 0ビット目が1の場合、Create
  • 1ビット目が1の場合、Write
  • 2ビット目が1の場合、Remove
  • 3ビット目が1の場合、Rename
  • 4ビット目が1の場合、Chmod

を意味します。

fsnotify.go
// These are the file operations that can trigger a notification.
const (
    Create Op = 1 << iota
    Write
    Remove
    Rename
    Chmod
)

Errors

Errors は、error 型のチャネルです。監視中に起こったエラーを表します。

paths

paths は、監視対象のフォルダのディレクトリを表します

(2)、(4)、(6)

(2) でブール型のチャネル変数 done を定義しています。

done := make(chan bool) // (2)

(6) で 変数 done に何らかの値が代入されるのを待ちます。

<-done // (6)

(4) で、ディレクトリの監視中に何らかのエラーが発生した場合に、変数 done に true が設定されます。その結果、(6) でチャネルの値を受信するため、このプログラム全体が終了することになります。

            case err:= <-watcher.Errors:
                log.Println("error: ", err)
                done <-true // (4)
            }

(3)

(3) で goroutine を作成し、この中でディレクトリの監視を行っています。

for 文でループを回し、watcher.Eventsから、ファイル変更の通知を受け取ると、それに応じた処理を行います。

    go func() { // (3)
        for {
            select {
            case event:= <-watcher.Events:
                log.Println("event: ", event)
                switch {
                case event.Op&fsnotify.Write == fsnotify.Write:
                    log.Println("Modified file: ", event.Name)
                case event.Op&fsnotify.Create == fsnotify.Create:
                    log.Println("Created file: ", event.Name)
                case event.Op&fsnotify.Remove == fsnotify.Remove:
                    log.Println("Removed file: ", event.Name)
                case event.Op&fsnotify.Rename == fsnotify.Rename:
                    log.Println("Renamed file: ", event.Name)
                case event.Op&fsnotify.Chmod == fsnotify.Chmod:
                    log.Println("File changed permission: ", event.Name)
                }
            case err:= <-watcher.Errors:
                log.Println("error: ", err)
                done <-true // (4)
            }
        }
    }()

event.Op には、ビット毎に、ファイル操作の意味が割り当てられています。たとえば、Write (1ビット目が1)だった場合は、以下の case が真になります。

                case event.Op&fsnotify.Write == fsnotify.Write:

(5)

Add メソッドで、監視対象のディレクトリのパスを設定しています。

err = watcher.Add(dirname) // (5)

fsnotify パッケージのバージョンについて

今回使用した fsnotify パッケージのバージョンは、v1 です。将来的に、API の変更が予定されているとのことですので、上記のサンプルコードを参考にする場合は、バージョンの違いに注意してください。

fsnotify パッケージの制限

上記のサンプルコードは、ディレクトリを階層的にたどって監視をすることができません。

例)
/Users/XXX/ を監視対象にした場合、/Users/XXX/aaa/ 内にあるファイルは監視対象になりません。

これは、fsnotify パッケージの内部で、kqueue API を使っているためです。kqueue は、BSD 系の OS (Mac OS X も含む)で使われている、ファイル変更検知のための API です。

一方で、Mac OS X の独自 API である FSEvents API を使えば、ファイル変更の監視を階層的に行うことができます。

fsnotify パッケージでは、FSEvents での実装も検討中とのことですので、将来的には、ディレクトリを階層的にたどって監視することができるようになるかもしれません。

API 対応 OS ディレクトリを階層的に監視できるか?
kqueue BSD 系 OS(Mac OS X も含む) ×
FSEvents Mac OS X

参考文献

Github - fsnotify

Advanced Mac OS X Programming: The Big Nerd Ranch Guide