Go/目录监听

在许多情景下, 我们需要有一种方式监听目录内的文件变化: 在开发 web 应用时, 修改源码后自动重启服务器; 修改源码后自动重新编译; 修改文件后自动上传文件至远端服务器等.

对于 Windows, 几乎所有目录监听程序使用的都是 Windows API 中的 ReadDirectoryChangesW 函数. 在本文之前, 使用 Golang 完成目录监听已经有部分轮子了, 但是无一例外这些轮子对文件系统事件进行了过度封装, 遂决定自己动手.

package main

import (
    "log"
    "syscall"
    "unsafe"
)

type FileNotifyInformation struct {
    Action uint32
    Name   string
}

func Fswatch(path string) (chan FileNotifyInformation, error) {
    handle, err := syscall.CreateFile(
        syscall.StringToUTF16Ptr(path),
        0x0001,
        syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
        nil,
        syscall.OPEN_EXISTING,
        syscall.FILE_FLAG_BACKUP_SEMANTICS,
        0,
    )
    if err != nil {
        return nil, err
    }

    c := make(chan FileNotifyInformation, 4)
    go func() {
        defer syscall.CloseHandle(handle)
        defer close(c)
        buflen := 1024
        buf := make([]byte, buflen)
        for {
            err := syscall.ReadDirectoryChanges(
                handle,
                &buf[0],
                uint32(buflen),
                true,
                syscall.FILE_NOTIFY_CHANGE_FILE_NAME|
                    syscall.FILE_NOTIFY_CHANGE_DIR_NAME|
                    syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES|
                    syscall.FILE_NOTIFY_CHANGE_SIZE|
                    syscall.FILE_NOTIFY_CHANGE_LAST_WRITE,
                nil,
                &syscall.Overlapped{},
                0,
            )
            if err != nil {
                break
            }

            var offset uint32
            for {
                raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&buf[offset]))
                buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName))
                name := syscall.UTF16ToString(buf[:raw.FileNameLength/2])
                info := FileNotifyInformation{
                    Action: raw.Action,
                    Name:   name,
                }
                c <- info
                if raw.NextEntryOffset == 0 {
                    break
                }
                offset += raw.NextEntryOffset
                if offset >= 1024 {
                    break
                }
            }
        }
    }()
    return c, nil
}

func main() {
    c, err := Fswatch("/tmp")
    if err != nil {
        log.Fatalln(err)
    }
    for info := range c {
        switch info.Action {
        case 1:
            log.Println("Create", info.Name)
        case 2:
            log.Println("Delete", info.Name)
        case 3:
            log.Println("Update", info.Name)
        case 4:
            log.Println("RenameFrom", info.Name)
        case 5:
            log.Println("RenameTo", info.Name)
        }

    }
}

上面的代码监听了 /tmp 目录下的文件系统事件, 如果对目录内的对象做了任何修改, 就可以看见对应的输出.