Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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. 監視対象の登録プロセス

  1. ディレクトリの特定: getDir()でディレクトリパスを決定
  2. Inode取得: getIno()でファイルシステム上の一意識別子を取得
  3. I/O Completion Portへの関連付け: CreateIoCompletionPort()で非同期I/O処理に登録
  4. 監視開始: 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
}

関連リンク

参考にした情報源リンク