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



# はじめに

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


# 開発環境

* fsnotify v1
* Go 1.3
* Mac OS X 10.9.3


# fsnotify パッケージとは？

ここでは、[fsnotify](https://github.com/go-fsnotify/fsnotify) という、サードパーティーのパッケージを使います。

fsnotify は、ファイルの変更を検知するためのパッケージです。今回は、Mac OS X 上でプログラムを実行しますが、Windows や Linux などの OS にも対応しています。[将来的には、標準パッケージへの統合を目指している](https://code.google.com/p/go/issues/detail?id=4068)とのことです。


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

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

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

## 注）環境変数 ```$GOPATH```

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

例（bashの場合）

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


# ソースコード

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

```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` 変数をとおして行っていきます。

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

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

```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` 型のフィールド変数のうち、重要なのは、以下の３つです。

* Events
* Errors
* paths


### Events

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

```go: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）を表します。

```go: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` には、ビット毎に、ファイル操作の意味が割り当てられており、

* ０ビット目が１の場合、`Create`
* １ビット目が１の場合、`Write`
* ２ビット目が１の場合、`Remove`
* ３ビット目が１の場合、`Rename`
* ４ビット目が１の場合、`Chmod`

を意味します。

```go: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` を定義しています。

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

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

```go
<-done // (6)
```

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

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


## (3)

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

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

```go:
    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` （１ビット目が１）だった場合は、以下の case が真になります。

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


## (5)

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

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


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

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


# fsnotify パッケージの制限

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

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

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

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

fsnotify パッケージでは、[FSEvents での実装も検討中](https://github.com/go-fsnotify/fsnotify/issues/11)とのことですので、将来的には、ディレクトリを階層的にたどって監視することができるようになるかもしれません。

|API     |対応 OS  |ディレクトリを階層的に監視できるか？|
|:-------|:-------|:----------------------------|
|kqueue  |BSD 系 OS（Mac OS X も含む）|×|
|FSEvents|Mac OS X|○|


# 参考文献

[Github - fsnotify](https://github.com/go-fsnotify/fsnotify)

[Advanced Mac OS X Programming: The Big Nerd Ranch Guide ](http://www.amazon.co.jp/Advanced-Mac-OS-Programming-Guides/dp/0321706250)