[インデックス 10041] ファイルの概要
コミット
このコミットは、Go言語の標準ライブラリに実験的なWindows向けファイルシステム監視機能を追加する重要な実装です。Hector Chu氏によって2011年10月18日に実装され、Windows上でファイルシステムの変更を監視するための包括的なソリューションを提供しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7ecf6c997e786b4812ae5d5f6afade15a3717fa9
元コミット内容
commit 7ecf6c997e786b4812ae5d5f6afade15a3717fa9
Author: Hector Chu <hectorchu@gmail.com>
Date: Tue Oct 18 21:09:58 2011 +0100
exp/winfsnotify: filesystem watcher for Windows
R=rsc, alex.brainman, bradfitz
CC=bsiegert, go.peter.90, golang-dev
https://golang.org/cl/4188047
このコミットでは以下の7つのファイルが新規作成・追加されています:
src/pkg/exp/winfsnotify/Makefile
(11行追加)src/pkg/exp/winfsnotify/winfsnotify.go
(569行追加)src/pkg/exp/winfsnotify/winfsnotify_test.go
(124行追加)src/pkg/syscall/syscall_windows.go
(2行追加)src/pkg/syscall/zsyscall_windows_386.go
(36行追加)src/pkg/syscall/zsyscall_windows_amd64.go
(36行追加)src/pkg/syscall/ztypes_windows.go
(30行追加)
合計808行のコードが追加されています。
変更の背景
2011年当時、Go言語はまだ誕生して間もない言語でした(Go 1.0が2012年3月にリリース)。この時期、ファイルシステム監視機能は多くのプログラムで必要とされる重要な機能でしたが、Go言語にはクロスプラットフォームのファイルシステム監視機能が存在しませんでした。
特にWindows環境では、Unixライクシステムで使用されるinotifyやkqueueのような機能が利用できないため、Windows独自のAPIを使用した実装が必要でした。この実装は、Go言語の実験的パッケージ(exp/
配下)として提供され、将来的な標準ライブラリへの統合を目的としていました。
このコミットは、Go言語がクロスプラットフォーム対応を強化し、Windows環境での実用性を向上させるための重要なステップでした。
前提知識の解説
Windows APIの基礎知識
ReadDirectoryChangesW API
Windows上でディレクトリの変更を監視する主要なAPIです。このAPIは以下の特徴を持ちます:
- 非同期操作をサポート
- 複数の変更タイプを同時に監視可能
- サブディレクトリの再帰監視をサポート
- UTF-16文字列を使用
I/O Completion Ports (IOCP)
Windowsの高性能非同期I/O処理機構です:
- 複数のI/O操作を効率的に処理
- スレッドプールとの統合
- 高いスケーラビリティ
- システムリソースの効率的な利用
Go言語の実験的パッケージ
2011年当時、Go言語では標準ライブラリに含める前に実験的パッケージ(exp/
配下)で機能を試験していました。これらのパッケージは:
- API設計の検証
- 実装の安定性確認
- コミュニティからのフィードバック収集
を目的としていました。
ファイルシステム監視の必要性
ファイルシステム監視機能は以下の用途で重要です:
- 開発ツール(自動リロード、ホットリロード)
- バックアップシステム
- ログファイル監視
- 設定ファイル変更の検出
- リアルタイムファイル同期
技術的詳細
アーキテクチャ設計
このWindows向けファイルシステム監視実装は、以下の主要コンポーネントで構成されています:
1. 非同期I/O処理アーキテクチャ
- I/O Completion Port: 高性能な非同期I/O処理の中核
- 専用I/Oスレッド:
runtime.LockOSThread()
でOSスレッドに固定 - チャンネルベース通信: Go言語の並行プリミティブを活用
2. 階層的な監視管理
- Volume-based indexing: ボリュームシリアル番号による階層化
- Inode-based tracking: ファイルインデックスによる一意識別
- Path-based filtering: パスベースの監視対象管理
3. イベント処理システム
- イベントバッファリング: 4KB固定サイズバッファ
- イベント変換: Windows API形式からGo構造体への変換
- イベントフィルタリング: 監視対象に応じたイベント選択
主要データ構造
Watcher構造体
type Watcher struct {
port syscall.Handle // I/O Completion Port
watches watchMap // 監視対象のマップ
input chan *input // 入力チャンネル
Event chan *Event // イベントチャンネル
Error chan os.Error // エラーチャンネル
isClosed bool // 終了フラグ
quit chan chan<- os.Error
cookie uint32 // 移動イベント用クッキー
}
watch構造体
type watch struct {
ov syscall.Overlapped // 非同期I/O用
ino *inode // inodeへのポインタ
path string // 監視ディレクトリパス
mask uint64 // 監視フラグ
names map[string]uint64 // ファイル名別監視フラグ
rename string // リネーム操作の旧名前
buf [4096]byte // イベントバッファ
}
inode構造体
type inode struct {
handle syscall.Handle // ファイルハンドル
volume uint32 // ボリュームシリアル番号
index uint64 // ファイルインデックス
}
監視メカニズム
1. 監視対象の登録プロセス
- ディレクトリの特定:
getDir()
でディレクトリパスを決定 - Inode取得:
getIno()
でファイルシステム上の一意識別子を取得 - I/O Completion Portへの関連付け:
CreateIoCompletionPort()
で非同期I/O処理に登録 - 監視開始:
ReadDirectoryChanges()
で実際の監視を開始
2. イベント処理ループ
func (w *Watcher) readEvents() {
runtime.LockOSThread() // OSスレッドに固定
for {
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
// イベント処理...
}
}
3. イベント変換処理
Windows APIから受信したイベントを、Go言語の構造体に変換:
switch raw.Action {
case syscall.FILE_ACTION_REMOVED:
mask = FS_DELETE_SELF
case syscall.FILE_ACTION_MODIFIED:
mask = FS_MODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
// リネーム処理...
mask = FS_MOVE_SELF
}
同期・非同期処理の統合
チャンネルベース通信
Go言語の並行プリミティブを活用し、同期的なAPIを提供しながら、内部では非同期処理を実装:
func (w *Watcher) AddWatch(path string, flags uint32) os.Error {
in := &input{
op: opAddWatch,
path: filepath.Clean(path),
flags: flags,
reply: make(chan os.Error),
}
w.input <- in
return <-in.reply // 同期的に結果を待機
}
I/Oスレッドでの処理
専用のI/Oスレッドで非同期的に処理し、結果をチャンネルで返却:
case in := <-w.input:
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags))
case opRemoveWatch:
in.reply <- w.removeWatch(in.path)
}
コアとなるコードの変更箇所
1. winfsnotify.go (569行)
メインの実装ファイル。以下の主要機能を提供:
src/pkg/exp/winfsnotify/winfsnotify.go:117-132
Watcherの初期化処理:
func NewWatcher() (*Watcher, os.Error) {
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
if e != 0 {
return nil, os.NewSyscallError("CreateIoCompletionPort", e)
}
w := &Watcher{
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
Event: make(chan *Event, 50),
Error: make(chan os.Error),
quit: make(chan chan<- os.Error, 1),
}
go w.readEvents()
return w, nil
}
src/pkg/exp/winfsnotify/winfsnotify.go:377-498
イベント処理の中核となるreadEvents()メソッド:
func (w *Watcher) readEvents() {
runtime.LockOSThread()
for {
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
// 制御メッセージ処理
select {
case ch := <-w.quit:
// 終了処理
case in := <-w.input:
// 入力処理
}
} else {
// ファイルシステムイベント処理
}
}
}
2. syscallへの追加 (104行)
Windows APIの呼び出し機能を追加:
src/pkg/syscall/syscall_windows.go:750
ReadDirectoryChanges APIの宣言:
//sys ReadDirectoryChanges(handle Handle, buf *byte, buflen uint32, watchSubTree bool, mask uint32, retlen *uint32, overlapped *Overlapped, completionRoutine uintptr) (errno int) = kernel32.ReadDirectoryChangesW
src/pkg/syscall/syscall_windows.go:750
PostQueuedCompletionStatus APIの宣言:
//sys PostQueuedCompletionStatus(cphandle Handle, qty uint32, key uint32, overlapped *Overlapped) (errno int)
3. 型定義の追加 (30行)
Windows固有の定数と構造体:
src/pkg/syscall/ztypes_windows.go:932-949
ファイル監視用の定数:
const (
FILE_NOTIFY_CHANGE_FILE_NAME = 1 << iota
FILE_NOTIFY_CHANGE_DIR_NAME
FILE_NOTIFY_CHANGE_ATTRIBUTES
FILE_NOTIFY_CHANGE_SIZE
FILE_NOTIFY_CHANGE_LAST_WRITE
FILE_NOTIFY_CHANGE_LAST_ACCESS
FILE_NOTIFY_CHANGE_CREATION
)
src/pkg/syscall/ztypes_windows.go:957-962
FileNotifyInformation構造体:
type FileNotifyInformation struct {
NextEntryOffset uint32
Action uint32
FileNameLength uint32
FileName uint16
}
4. テストファイル (124行)
包括的なテストケースを提供:
src/pkg/exp/winfsnotify/winfsnotify_test.go:646-720
主要な機能テスト:
- ファイル作成の監視
- ファイル変更の監視
- ファイル移動の監視
- ディレクトリ削除の監視
- 二重Close()の処理
コアとなるコードの解説
1. 非同期I/O処理アーキテクチャ
I/O Completion Portの活用
func NewWatcher() (*Watcher, os.Error) {
// I/O Completion Portを作成
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
if e != 0 {
return nil, os.NewSyscallError("CreateIoCompletionPort", e)
}
// Watcherを初期化
w := &Watcher{
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
Event: make(chan *Event, 50), // イベントバッファ
Error: make(chan os.Error),
quit: make(chan chan<- os.Error, 1),
}
// 専用I/Oスレッドを起動
go w.readEvents()
return w, nil
}
この設計により、複数のファイルシステム監視を単一のI/Oスレッドで効率的に処理できます。
専用I/Oスレッドの実装
func (w *Watcher) readEvents() {
// OSスレッドに固定(WindowsのI/O Completion Portはスレッドアフィニティが重要)
runtime.LockOSThread()
for {
// I/O Completion Portからイベントを取得
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
// 制御メッセージの処理
select {
case ch := <-w.quit:
// 終了処理
for _, index := range w.watches {
for _, watch := range index {
w.deleteWatch(watch)
w.startRead(watch)
}
}
// リソースクリーンアップ
var err os.Error
if e := syscall.CloseHandle(w.port); e != 0 {
err = os.NewSyscallError("CloseHandle", e)
}
close(w.Event)
close(w.Error)
ch <- err
return
case in := <-w.input:
// 監視追加・削除の処理
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags))
case opRemoveWatch:
in.reply <- w.removeWatch(in.path)
}
}
} else {
// ファイルシステムイベントの処理
// ... イベント処理ロジック
}
}
}
2. 階層的な監視管理システム
Volume-Inode-based インデックス
type indexMap map[uint64]*watch // ファイルインデックス -> watch
type watchMap map[uint32]indexMap // ボリュームシリアル番号 -> indexMap
func (m watchMap) get(ino *inode) *watch {
if i := m[ino.volume]; i != nil {
return i[ino.index]
}
return nil
}
func (m watchMap) set(ino *inode, watch *watch) {
i := m[ino.volume]
if i == nil {
i = make(indexMap)
m[ino.volume] = i
}
i[ino.index] = watch
}
この階層構造により、異なるボリューム上の同じファイルインデックスを持つファイルを正確に識別できます。
Inode取得処理
func getIno(path string) (ino *inode, err os.Error) {
// ディレクトリハンドルを取得
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path),
syscall.FILE_LIST_DIRECTORY,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0)
if e != 0 {
return nil, os.NewSyscallError("CreateFile", e)
}
// ファイル情報を取得
var fi syscall.ByHandleFileInformation
if e = syscall.GetFileInformationByHandle(h, &fi); e != 0 {
syscall.CloseHandle(h)
return nil, os.NewSyscallError("GetFileInformationByHandle", e)
}
// inode構造体を作成
ino = &inode{
handle: h,
volume: fi.VolumeSerialNumber,
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
}
return ino, nil
}
3. イベント処理とフィルタリング
Windows APIイベントの変換
func toFSnotifyFlags(action uint32) uint64 {
switch action {
case syscall.FILE_ACTION_ADDED:
return FS_CREATE
case syscall.FILE_ACTION_REMOVED:
return FS_DELETE
case syscall.FILE_ACTION_MODIFIED:
return FS_MODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
return FS_MOVED_FROM
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
return FS_MOVED_TO
}
return 0
}
func toWindowsFlags(mask uint64) uint32 {
var m uint32
if mask&FS_ACCESS != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS
}
if mask&FS_MODIFY != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE
}
if mask&FS_ATTRIB != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(FS_MOVE|FS_CREATE|FS_DELETE) != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME
}
return m
}
複雑なリネーム処理
// readEvents()内でのリネーム処理
switch raw.Action {
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name // 旧名前を記録
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
if watch.names[watch.rename] != 0 {
// 旧名前の監視設定を新名前に移行
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = FS_MOVE_SELF
}
}
4. 同期処理の実装
公開API - 同期的インターフェース
func (w *Watcher) AddWatch(path string, flags uint32) os.Error {
if w.isClosed {
return os.NewError("watcher already closed")
}
// 入力構造体を作成
in := &input{
op: opAddWatch,
path: filepath.Clean(path),
flags: flags,
reply: make(chan os.Error),
}
// I/Oスレッドに要求を送信
w.input <- in
// I/Oスレッドを起動
if err := w.wakeupReader(); err != nil {
return err
}
// 同期的に結果を待機
return <-in.reply
}
I/Oスレッドでの非同期処理
func (w *Watcher) addWatch(pathname string, flags uint64) os.Error {
// ディレクトリを取得
dir, err := getDir(pathname)
if err != nil {
return err
}
// ONLYDIR フラグの処理
if flags&FS_ONLYDIR != 0 && pathname != dir {
return nil
}
// inode を取得
ino, err := getIno(dir)
if err != nil {
return err
}
// 既存の監視を確認
watchEntry := w.watches.get(ino)
if watchEntry == nil {
// 新規監視の作成
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != 0 {
syscall.CloseHandle(ino.handle)
return os.NewSyscallError("CreateIoCompletionPort", e)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
}
w.watches.set(ino, watchEntry)
flags |= provisional
} else {
// 既存の監視を更新
syscall.CloseHandle(ino.handle)
}
// 監視対象の更新
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
// 監視開始
if err = w.startRead(watchEntry); err != nil {
return err
}
// provisional フラグをクリア
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
return nil
}
5. リソース管理と例外処理
安全なリソース管理
func (w *Watcher) Close() os.Error {
if w.isClosed {
return nil
}
w.isClosed = true
// 終了メッセージを送信
ch := make(chan os.Error)
w.quit <- ch
// I/Oスレッドを起動
if err := w.wakeupReader(); err != nil {
return err
}
// 終了完了を待機
return <-ch
}
エラーハンドリング
func (w *Watcher) startRead(watch *watch) os.Error {
// 既存のI/O操作をキャンセル
if e := syscall.CancelIo(watch.ino.handle); e != 0 {
w.Error <- os.NewSyscallError("CancelIo", e)
w.deleteWatch(watch)
}
// 監視マスクを計算
mask := toWindowsFlags(watch.mask)
for _, m := range watch.names {
mask |= toWindowsFlags(m)
}
// 監視対象がない場合はクリーンアップ
if mask == 0 {
if e := syscall.CloseHandle(watch.ino.handle); e != 0 {
w.Error <- os.NewSyscallError("CloseHandle", e)
}
delete(w.watches[watch.ino.volume], watch.ino.index)
return nil
}
// ReadDirectoryChanges を開始
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
if e != 0 {
err := os.NewSyscallError("ReadDirectoryChanges", e)
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// 監視ディレクトリが削除された場合
if w.sendEvent(watch.path, watch.mask&FS_DELETE_SELF) {
if watch.mask&FS_ONESHOT != 0 {
watch.mask = 0
}
}
err = nil
}
w.deleteWatch(watch)
w.startRead(watch)
return err
}
return nil
}
関連リンク
- Go言語公式 - Go 1.0リリースノート
- Microsoft Learn - ReadDirectoryChangesW API
- Microsoft Learn - I/O Completion Ports
- fsnotify/fsnotify - 現在のクロスプラットフォーム実装
- Go言語 - 実験的パッケージのドキュメント
参考にした情報源リンク
- Jim Beveridge's Blog - Understanding ReadDirectoryChangesW
- Tresorit Engineering - Using ReadDirectoryChangesW() on Windows
- Microsoft Learn - Obtaining Directory Change Notifications
- Go言語 Issue #4068 - os/fsnotify: add new package
- GitHub - AlgoPeek/ReadDirectoryChanges
- GitHub - GerHobbelt/ReadDirectoryChangesIOCP